From 85795f5406699771b138ee57d2955498bd155033 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Thu, 28 Mar 2024 21:25:59 +0100 Subject: [PATCH 001/114] Changed GetUserProjects so that you have to get username from params. Now admin can choose a user and see what projects the user belongs to --- backend/internal/handlers/handlers_project_related.go | 9 +++++---- backend/main.go | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) 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/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 From b036ef906c4251ce0c0498ac657f2a9b759b7362 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Thu, 28 Mar 2024 21:31:30 +0100 Subject: [PATCH 002/114] Small fixes in all files that fetches user projects, so that they pass username as argument to GetProjects-function --- frontend/src/API/API.ts | 13 ++++++++--- .../src/Components/DisplayUserProjects.tsx | 23 +++++-------------- frontend/src/Components/GetProjects.tsx | 5 ++-- frontend/src/Components/UserInfoModal.tsx | 2 +- .../src/Components/UserProjectListAdmin.tsx | 8 +++---- .../Pages/AdminPages/AdminManageProjects.tsx | 5 +++- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 41be31a..39b5d0a 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -114,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. @@ -302,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", 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/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index 9695899..9d8dc11 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -42,7 +42,7 @@ function UserInfoModal(props: { Member of these projects:
- +
diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index 1b7b923..5335f1b 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -2,16 +2,16 @@ import { useEffect, useState } from "react"; import { api } from "../API/API"; import { Project } from "../Types/goTypes"; -function UserProjectListAdmin(): JSX.Element { +function UserProjectListAdmin(props: { username: string }): JSX.Element { const [projects, setProjects] = useState([]); useEffect(() => { const fetchProjects = async (): Promise => { try { const token = localStorage.getItem("accessToken") ?? ""; - // const username = props.username; + const username = props.username; - const response = await api.getUserProjects(token); + const response = await api.getUserProjects(username, token); if (response.success) { setProjects(response.data ?? []); } else { @@ -23,7 +23,7 @@ function UserProjectListAdmin(): JSX.Element { }; void fetchProjects(); - }, []); + }, [props.username]); return (
diff --git a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx index 7ea45df..6c03c01 100644 --- a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx @@ -9,7 +9,10 @@ import { useState } from "react"; function AdminManageProjects(): JSX.Element { const [projects, setProjects] = useState([]); - GetProjects({ setProjectsProp: setProjects }); + GetProjects({ + setProjectsProp: setProjects, + username: localStorage.getItem("username") ?? "", + }); const content = ( <>

Manage Projects

From 13d3035e49a8cef39fba84d6db095855df83051a Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 14:37:22 +0100 Subject: [PATCH 003/114] Major refactor, splitting user handlers into separate files and changes to how the database is accessed --- backend/internal/database/middleware.go | 17 ++ backend/internal/handlers/global_state.go | 44 --- .../internal/handlers/global_state_test.go | 15 - .../handlers/handlers_user_related.go | 269 ------------------ .../handlers_project_related.go | 57 ++-- .../{ => reports}/handlers_report_related.go | 31 +- .../internal/handlers/users/ChangeUserName.go | 44 +++ .../handlers/users/GetUsersProjects.go | 22 ++ .../internal/handlers/users/ListAllUsers.go | 31 ++ backend/internal/handlers/users/Login.go | 65 +++++ backend/internal/handlers/users/LoginRenew.go | 44 +++ .../internal/handlers/users/PromoteToAdmin.go | 42 +++ backend/internal/handlers/users/Register.go | 38 +++ backend/internal/handlers/users/UserDelete.go | 43 +++ backend/main.go | 77 +++-- 15 files changed, 439 insertions(+), 400 deletions(-) create mode 100644 backend/internal/database/middleware.go delete mode 100644 backend/internal/handlers/global_state.go delete mode 100644 backend/internal/handlers/global_state_test.go delete mode 100644 backend/internal/handlers/handlers_user_related.go rename backend/internal/handlers/{ => projects}/handlers_project_related.go (83%) rename backend/internal/handlers/{ => reports}/handlers_report_related.go (83%) create mode 100644 backend/internal/handlers/users/ChangeUserName.go create mode 100644 backend/internal/handlers/users/GetUsersProjects.go create mode 100644 backend/internal/handlers/users/ListAllUsers.go create mode 100644 backend/internal/handlers/users/Login.go create mode 100644 backend/internal/handlers/users/LoginRenew.go create mode 100644 backend/internal/handlers/users/PromoteToAdmin.go create mode 100644 backend/internal/handlers/users/Register.go create mode 100644 backend/internal/handlers/users/UserDelete.go diff --git a/backend/internal/database/middleware.go b/backend/internal/database/middleware.go new file mode 100644 index 0000000..69fa3a2 --- /dev/null +++ b/backend/internal/database/middleware.go @@ -0,0 +1,17 @@ +package database + +import "github.com/gofiber/fiber/v2" + +// Simple middleware that provides a shared database pool as a local key "db" +func DbMiddleware(db *Database) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Locals("db", db) + return c.Next() + } +} + +// Helper function to get the database from the context, without fiddling with casts +func GetDb(c *fiber.Ctx) Database { + // Dereference a pointer to a local, casted to a pointer to a Database + return *c.Locals("db").(*Database) +} diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go deleted file mode 100644 index 0db4340..0000000 --- a/backend/internal/handlers/global_state.go +++ /dev/null @@ -1,44 +0,0 @@ -package handlers - -import ( - "ttime/internal/database" - - "github.com/gofiber/fiber/v2" -) - -// The actual interface that we will use -type GlobalState interface { - Register(c *fiber.Ctx) error // To register a new user - UserDelete(c *fiber.Ctx) error // To delete a user - Login(c *fiber.Ctx) error // To get the token - LoginRenew(c *fiber.Ctx) error // To renew the token - CreateProject(c *fiber.Ctx) error // To create a new project - GetUserProjects(c *fiber.Ctx) error // To get all projects - SubmitWeeklyReport(c *fiber.Ctx) error - GetWeeklyReport(c *fiber.Ctx) error - SignReport(c *fiber.Ctx) error - GetProject(c *fiber.Ctx) error - AddUserToProjectHandler(c *fiber.Ctx) error - PromoteToAdmin(c *fiber.Ctx) error - GetWeeklyReportsUserHandler(c *fiber.Ctx) error - IsProjectManagerHandler(c *fiber.Ctx) error - DeleteProject(c *fiber.Ctx) error // To delete a project // WIP - ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database - ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project - ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project - ChangeUserName(c *fiber.Ctx) error // WIP - GetAllUsersProject(c *fiber.Ctx) error // WIP - GetUnsignedReports(c *fiber.Ctx) error // - UpdateWeeklyReport(c *fiber.Ctx) error - RemoveProject(c *fiber.Ctx) error -} - -// "Constructor" -func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db} -} - -// The global state, which implements all the handlers -type GState struct { - Db database.Database -} diff --git a/backend/internal/handlers/global_state_test.go b/backend/internal/handlers/global_state_test.go deleted file mode 100644 index c0b64f7..0000000 --- a/backend/internal/handlers/global_state_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package handlers - -import ( - "testing" - "ttime/internal/database" -) - -// The actual interface that we will use -func TestGlobalState(t *testing.T) { - db := database.DbConnect(":memory:") - gs := NewGlobalState(db) - if gs == nil { - t.Error("NewGlobalState returned nil") - } -} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go deleted file mode 100644 index bc4ae2d..0000000 --- a/backend/internal/handlers/handlers_user_related.go +++ /dev/null @@ -1,269 +0,0 @@ -package handlers - -import ( - "time" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2/log" - - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" -) - -// Register is a simple handler that registers a new user -// -// @Summary Register -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "User to register" -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /register [post] -func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Adding user:", u.Username) - if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - log.Warn("Error adding user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User added:", u.Username) - return c.Status(200).SendString("User added") -} - -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -// -// @Summary UserDelete -// @Description UserDelete deletes a user from the database -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {string} string "User deleted" -// @Failure 403 {string} string "You can only delete yourself" -// @Failure 500 {string} string "Internal server error" -// @Failure 401 {string} string "Unauthorized" -// @Router /userdelete/{username} [delete] -func (gs *GState) UserDelete(c *fiber.Ctx) error { - // Read from path parameters - username := c.Params("username") - - // 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 itself") - return c.Status(403).SendString("You can't delete yourself") - } - - if err := gs.Db.RemoveUser(username); err != nil { - log.Warn("Error deleting user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User deleted:", username) - return c.Status(200).SendString("User deleted") -} - -// Login is a simple login handler that returns a JWT token -// -// @Summary login -// @Description logs the user in and returns a jwt token -// @Tags User -// @Accept json -// @Param NewUser body types.NewUser true "login info" -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] -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 { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Username logging in:", u.Username) - if !gs.Db.CheckUser(u.Username, u.Password) { - log.Info("User not found") - return c.SendStatus(fiber.StatusUnauthorized) - } - - isAdmin, err := gs.Db.IsSiteAdmin(u.Username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - // Create the Claims - claims := jwt.MapClaims{ - "name": u.Username, - "admin": isAdmin, - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - log.Info("Token created for user:", u.Username) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - println("Successfully signed token for user:", u.Username) - return c.JSON(types.Token{Token: t}) -} - -// LoginRenew is a simple handler that renews the token -// -// @Summary LoginRenews -// @Description renews the users token -// @Security bererToken -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginerenew [post] -func (gs *GState) LoginRenew(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - - claims := user.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - renewed := jwt.MapClaims{ - "name": claims["name"], - "admin": claims["admin"], - "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) - return c.JSON(types.Token{Token: t}) -} - -// ListAllUsers is a handler that returns a list of all users in the application database -// -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {json} json "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] -func (gs *GState) ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := gs.Db.GetAllUsersApplication() - if err != nil { - log.Info("Error getting users from db:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning all users") - // Return the list of users as JSON - return c.JSON(users) -} - -func (gs *GState) GetAllUsersProject(c *fiber.Ctx) error { - // Get all users from a project - projectName := c.Params("projectName") - users, err := gs.Db.GetAllUsersProject(projectName) - if err != nil { - log.Info("Error getting users from project:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning all users") - // Return the list of users as JSON - return c.JSON(users) -} - -// @Summary PromoteToAdmin -// @Description promote chosen user to admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] -func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { - // Extract the username from the request body - var newUser types.NewUser - if err := c.BodyParser(&newUser); err != nil { - return c.Status(400).SendString("Bad request") - } - username := newUser.Username - - log.Info("Promoting user to admin:", username) // Debug print - - // Promote the user to a site admin in the database - if err := gs.Db.PromoteToAdmin(username); err != nil { - log.Info("Error promoting user to admin:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("User promoted to admin successfully:", username) // Debug print - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} - -// ChangeUserName changes a user's username in the database -func (gs *GState) ChangeUserName(c *fiber.Ctx) error { - // Check token and get username of current user - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - adminUsername := claims["name"].(string) - log.Info(adminUsername) - - // Extract the necessary parameters from the request - data := new(types.StrNameChange) - if err := c.BodyParser(data); err != nil { - log.Info("Error parsing username") - return c.Status(400).SendString(err.Error()) - } - - // Check if the current user is an admin - isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) - if err != nil { - log.Warn("Error checking if admin:", err) - return c.Status(500).SendString(err.Error()) - } else if !isAdmin { - log.Warn("Tried changing name when not admin") - return c.Status(401).SendString("You cannot change name unless you are an admin") - } - - // Change the user's name in the database - if err := gs.Db.ChangeUserName(data.PrevName, data.NewName); err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/projects/handlers_project_related.go similarity index 83% rename from backend/internal/handlers/handlers_project_related.go rename to backend/internal/handlers/projects/handlers_project_related.go index d63d7eb..3429504 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/projects/handlers_project_related.go @@ -1,7 +1,8 @@ -package handlers +package projects import ( "strconv" + db "ttime/internal/database" "ttime/internal/types" "github.com/gofiber/fiber/v2" @@ -10,7 +11,7 @@ import ( ) // CreateProject is a simple handler that creates a new project -func (gs *GState) CreateProject(c *fiber.Ctx) error { +func CreateProject(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) p := new(types.NewProject) @@ -23,19 +24,19 @@ func (gs *GState) CreateProject(c *fiber.Ctx) error { claims := user.Claims.(jwt.MapClaims) owner := claims["name"].(string) - if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { + if err := db.GetDb(c).AddProject(p.Name, p.Description, owner); err != nil { return c.Status(500).SendString(err.Error()) } return c.Status(200).SendString("Project added") } -func (gs *GState) DeleteProject(c *fiber.Ctx) error { +func DeleteProject(c *fiber.Ctx) error { projectID := c.Params("projectID") username := c.Params("username") - if err := gs.Db.DeleteProject(projectID, username); err != nil { + if err := db.GetDb(c).DeleteProject(projectID, username); err != nil { return c.Status(500).SendString((err.Error())) } @@ -43,14 +44,14 @@ 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 { +func 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) // Then dip into the database to get the projects - projects, err := gs.Db.GetProjectsForUser(username) + projects, err := db.GetDb(c).GetProjectsForUser(username) if err != nil { return c.Status(500).SendString(err.Error()) } @@ -60,7 +61,7 @@ func (gs *GState) GetUserProjects(c *fiber.Ctx) error { } // ProjectRoleChange is a handler that changes a user's role within a project -func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { +func ProjectRoleChange(c *fiber.Ctx) error { //check token and get username of current user user := c.Locals("user").(*jwt.Token) @@ -77,7 +78,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) // Dubble diping and checcking if current user is - if ismanager, err := gs.Db.IsProjectManager(username, data.Projectname); err != nil { + if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil { log.Warn("Error checking if projectmanager:", err) return c.Status(500).SendString(err.Error()) } else if !ismanager { @@ -86,7 +87,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { } // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, data.Projectname, data.Role); err != nil { + if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { return c.Status(500).SendString(err.Error()) } @@ -95,7 +96,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { } // GetProject retrieves a specific project by its ID -func (gs *GState) GetProject(c *fiber.Ctx) error { +func GetProject(c *fiber.Ctx) error { // Extract the project ID from the request parameters or body projectID := c.Params("projectID") if projectID == "" { @@ -112,7 +113,7 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { } // Get the project from the database by its ID - project, err := gs.Db.GetProject(projectIDInt) + project, err := db.GetDb(c).GetProject(projectIDInt) if err != nil { log.Info("Error getting project:", err) return c.Status(500).SendString(err.Error()) @@ -123,7 +124,7 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { return c.JSON(project) } -func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { +func ListAllUsersProject(c *fiber.Ctx) error { // Extract the project name from the request parameters or body projectName := c.Params("projectName") if projectName == "" { @@ -137,7 +138,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { username := claims["name"].(string) // Check if the user is a project manager for the specified project - isManager, err := gs.Db.IsProjectManager(username, projectName) + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) if err != nil { log.Info("Error checking project manager status:", err) return c.Status(500).SendString(err.Error()) @@ -145,7 +146,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { // If the user is not a project manager, check if the user is a site admin if !isManager { - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -157,7 +158,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { } // Get all users associated with the project from the database - users, err := gs.Db.GetAllUsersProject(projectName) + users, err := db.GetDb(c).GetAllUsersProject(projectName) if err != nil { log.Info("Error getting users for project:", err) return c.Status(500).SendString(err.Error()) @@ -170,7 +171,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { } // AddUserToProjectHandler is a handler that adds a user to a project with a specified role -func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { +func AddUserToProjectHandler(c *fiber.Ctx) error { // Extract necessary parameters from the request var requestData struct { Username string `json:"username"` @@ -188,7 +189,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { adminUsername := claims["name"].(string) log.Info("Admin username from claims:", adminUsername) - isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -200,7 +201,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { } // Add the user to the project with the specified role - err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) if err != nil { log.Info("Error adding user to project:", err) return c.Status(500).SendString(err.Error()) @@ -212,7 +213,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { } // IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project -func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { +func IsProjectManagerHandler(c *fiber.Ctx) error { // Get the username from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -224,7 +225,7 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { log.Info("Checking if user ", username, " is a project manager for project ", projectName) // Check if the user is a project manager for the specified project - isManager, err := gs.Db.IsProjectManager(username, projectName) + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) if err != nil { log.Info("Error checking project manager status:", err) return c.Status(500).SendString(err.Error()) @@ -234,7 +235,7 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"isProjectManager": isManager}) } -func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { +func GetProjectTimesHandler(c *fiber.Ctx) error { // Get the username from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -248,7 +249,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { } // Get all users in the project and roles - userProjects, err := gs.Db.GetAllUsersProject(projectName) + userProjects, err := db.GetDb(c).GetAllUsersProject(projectName) if err != nil { log.Info("Error getting users in project:", err) return c.Status(500).SendString(err.Error()) @@ -265,7 +266,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { // If the user is admin if !isMember { - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -277,7 +278,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { } // Get project times - projectTimes, err := gs.Db.GetProjectTimes(projectName) + projectTimes, err := db.GetDb(c).GetProjectTimes(projectName) if err != nil { log.Info("Error getting project times:", err) return c.Status(500).SendString(err.Error()) @@ -288,13 +289,13 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { return c.JSON(projectTimes) } -func (gs *GState) RemoveProject(c *fiber.Ctx) error { +func RemoveProject(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) username := claims["name"].(string) // Check if the user is a site admin - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -307,7 +308,7 @@ func (gs *GState) RemoveProject(c *fiber.Ctx) error { projectName := c.Params("projectName") - if err := gs.Db.RemoveProject(projectName); err != nil { + if err := db.GetDb(c).RemoveProject(projectName); err != nil { return c.Status(500).SendString((err.Error())) } diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/reports/handlers_report_related.go similarity index 83% rename from backend/internal/handlers/handlers_report_related.go rename to backend/internal/handlers/reports/handlers_report_related.go index 52e1564..1c84d52 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/reports/handlers_report_related.go @@ -1,7 +1,8 @@ -package handlers +package reports import ( "strconv" + db "ttime/internal/database" "ttime/internal/types" "github.com/gofiber/fiber/v2" @@ -9,7 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" ) -func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { +func SubmitWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -31,7 +32,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { return c.Status(400).SendString("Invalid time report") } - if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { + if err := db.GetDb(c).AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { log.Info("Error adding weekly report to db:", err) return c.Status(500).SendString(err.Error()) } @@ -41,7 +42,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { } // Handler for retrieving weekly report -func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { +func GetWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the request user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -66,7 +67,7 @@ func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { } // Call the database function to get the weekly report - report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) + report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt) if err != nil { log.Info("Error getting weekly report from db:", err) return c.Status(500).SendString(err.Error()) @@ -81,7 +82,7 @@ type ReportId struct { ReportId int } -func (gs *GState) SignReport(c *fiber.Ctx) error { +func SignReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -98,7 +99,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { log.Info("Signing report for: ", rid.ReportId) // Get the project manager's ID - projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) + projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) if err != nil { log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") @@ -106,7 +107,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { log.Info("Project manager ID: ", projectManagerID) // Call the database function to sign the weekly report - err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) + err = db.GetDb(c).SignWeeklyReport(rid.ReportId, projectManagerID) if err != nil { log.Info("Error signing weekly report:", err) return c.Status(500).SendString(err.Error()) @@ -115,7 +116,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { return c.Status(200).SendString("Weekly report signed successfully") } -func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { +func GetUnsignedReports(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -132,7 +133,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { } // Get the project manager's ID - isProjectManager, err := gs.Db.IsProjectManager(projectManagerUsername, projectName) + isProjectManager, err := db.GetDb(c).IsProjectManager(projectManagerUsername, projectName) if err != nil { log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") @@ -140,7 +141,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { log.Info("User is Project Manager: ", isProjectManager) // Call the database function to get the unsigned weekly reports - reports, err := gs.Db.GetUnsignedWeeklyReports(projectName) + reports, err := db.GetDb(c).GetUnsignedWeeklyReports(projectName) if err != nil { log.Info("Error getting unsigned weekly reports:", err) return c.Status(500).SendString(err.Error()) @@ -152,7 +153,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { } // GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project -func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { +func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -166,7 +167,7 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { // the returned list of reports will (should) allways be empty. // Retrieve weekly reports for the user in the project from the database - reports, err := gs.Db.GetWeeklyReportsUser(username, projectName) + reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName) if err != nil { log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) return c.Status(500).SendString(err.Error()) @@ -178,7 +179,7 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { return c.JSON(reports) } -func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error { +func UpdateWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -203,7 +204,7 @@ func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error { } // Update the weekly report in the database - if err := gs.Db.UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { + if err := db.GetDb(c).UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { log.Info("Error updating weekly report in db:", err) return c.Status(500).SendString(err.Error()) } diff --git a/backend/internal/handlers/users/ChangeUserName.go b/backend/internal/handlers/users/ChangeUserName.go new file mode 100644 index 0000000..75032e4 --- /dev/null +++ b/backend/internal/handlers/users/ChangeUserName.go @@ -0,0 +1,44 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeUserName changes a user's username in the database +func ChangeUserName(c *fiber.Ctx) error { + // Check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info(adminUsername) + + // Extract the necessary parameters from the request + data := new(types.StrNameChange) + if err := c.BodyParser(data); err != nil { + log.Info("Error parsing username") + return c.Status(400).SendString(err.Error()) + } + + // Check if the current user is an admin + isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) + if err != nil { + log.Warn("Error checking if admin:", err) + return c.Status(500).SendString(err.Error()) + } else if !isAdmin { + log.Warn("Tried changing name when not admin") + return c.Status(401).SendString("You cannot change name unless you are an admin") + } + + // Change the user's name in the database + if err := db.GetDb(c).ChangeUserName(data.PrevName, data.NewName); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/users/GetUsersProjects.go b/backend/internal/handlers/users/GetUsersProjects.go new file mode 100644 index 0000000..10a6ec6 --- /dev/null +++ b/backend/internal/handlers/users/GetUsersProjects.go @@ -0,0 +1,22 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +func GetAllUsersProject(c *fiber.Ctx) error { + // Get all users from a project + projectName := c.Params("projectName") + users, err := db.GetDb(c).GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users from project:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning all users") + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/users/ListAllUsers.go b/backend/internal/handlers/users/ListAllUsers.go new file mode 100644 index 0000000..1cae76c --- /dev/null +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -0,0 +1,31 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// ListAllUsers is a handler that returns a list of all users in the application database +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {json} json "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] +func ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := db.GetDb(c).GetAllUsersApplication() + if err != nil { + log.Info("Error getting users from db:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning all users") + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go new file mode 100644 index 0000000..c4d6c60 --- /dev/null +++ b/backend/internal/handlers/users/Login.go @@ -0,0 +1,65 @@ +package users + +import ( + "time" + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// Login is a simple login handler that returns a JWT token +// @Summary login +// @Description logs the user in and returns a jwt token +// @Tags User +// @Accept json +// @Param NewUser body types.NewUser true "login info" +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] +func Login(c *fiber.Ctx) error { + // The body type is identical to a NewUser + + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + log.Warn("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Username logging in:", u.Username) + if !db.GetDb(c).CheckUser(u.Username, u.Password) { + log.Info("User not found") + return c.SendStatus(fiber.StatusUnauthorized) + } + + isAdmin, err := db.GetDb(c).IsSiteAdmin(u.Username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + // Create the Claims + claims := jwt.MapClaims{ + "name": u.Username, + "admin": isAdmin, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + log.Info("Token created for user:", u.Username) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + log.Warn("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) + } + + println("Successfully signed token for user:", u.Username) + return c.JSON(types.Token{Token: t}) +} diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go new file mode 100644 index 0000000..78eadfd --- /dev/null +++ b/backend/internal/handlers/users/LoginRenew.go @@ -0,0 +1,44 @@ +package users + +import ( + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// LoginRenew is a simple handler that renews the token +// @Summary LoginRenews +// @Description renews the users token +// @Security bererToken +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginerenew [post] +func LoginRenew(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) + + claims := user.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + renewed := jwt.MapClaims{ + "name": claims["name"], + "admin": claims["admin"], + "exp": claims["exp"], + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + t, err := token.SignedString([]byte("secret")) + if err != nil { + log.Warn("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) + } + + log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) + return c.JSON(types.Token{Token: t}) +} diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go new file mode 100644 index 0000000..4a21758 --- /dev/null +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -0,0 +1,42 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// @Summary PromoteToAdmin +// @Description promote chosen user to admin +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] +func PromoteToAdmin(c *fiber.Ctx) error { + // Extract the username from the request body + var newUser types.NewUser + if err := c.BodyParser(&newUser); err != nil { + return c.Status(400).SendString("Bad request") + } + username := newUser.Username + + log.Info("Promoting user to admin:", username) // Debug print + + // Promote the user to a site admin in the database + if err := db.GetDb(c).PromoteToAdmin(username); err != nil { + log.Info("Error promoting user to admin:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("User promoted to admin successfully:", username) // Debug print + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go new file mode 100644 index 0000000..9977246 --- /dev/null +++ b/backend/internal/handlers/users/Register.go @@ -0,0 +1,38 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// Register is a simple handler that registers a new user +// +// @Summary Register +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "User to register" +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /register [post] +func Register(c *fiber.Ctx) error { + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + log.Warn("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Adding user:", u.Username) + if err := db.GetDb(c).AddUser(u.Username, u.Password); err != nil { + log.Warn("Error adding user:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("User added:", u.Username) + return c.Status(200).SendString("User added") +} diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go new file mode 100644 index 0000000..5957c2d --- /dev/null +++ b/backend/internal/handlers/users/UserDelete.go @@ -0,0 +1,43 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +// +// @Summary UserDelete +// @Description UserDelete deletes a user from the database +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {string} string "User deleted" +// @Failure 403 {string} string "You can only delete yourself" +// @Failure 500 {string} string "Internal server error" +// @Failure 401 {string} string "Unauthorized" +// @Router /userdelete/{username} [delete] +func UserDelete(c *fiber.Ctx) error { + // Read from path parameters + username := c.Params("username") + + // 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 itself") + return c.Status(403).SendString("You can't delete yourself") + } + + if err := db.GetDb(c).RemoveUser(username); err != nil { + log.Warn("Error deleting user:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("User deleted:", username) + return c.Status(200).SendString("User deleted") +} diff --git a/backend/main.go b/backend/main.go index 669bbc7..ebe5660 100644 --- a/backend/main.go +++ b/backend/main.go @@ -6,7 +6,9 @@ import ( _ "ttime/docs" "ttime/internal/config" "ttime/internal/database" - "ttime/internal/handlers" + "ttime/internal/handlers/projects" + "ttime/internal/handlers/reports" + "ttime/internal/handlers/users" "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" @@ -54,24 +56,28 @@ func main() { // Connect to the database db := database.DbConnect(conf.DbPath) + // Migrate the database if err = db.Migrate(); err != nil { fmt.Println("Error migrating database: ", err) os.Exit(1) } + // Migrate sample data, should not be used in production if err = db.MigrateSampleData(); err != nil { fmt.Println("Error migrating sample data: ", err) os.Exit(1) } - // Get our global state - gs := handlers.NewGlobalState(db) // Create the server server := fiber.New() + // We want some logs server.Use(logger.New()) + // Sets up db middleware, accessed as Local "db" key + server.Use(database.DbMiddleware(&db)) + // Mounts the swagger documentation, this is available at /swagger/index.html server.Get("/swagger/*", swagger.HandlerDefault) @@ -79,37 +85,50 @@ func main() { // This will likely be replaced by an embedded filesystem in the future server.Static("/", "./static") - // Register our unprotected routes - server.Post("/api/register", gs.Register) - server.Post("/api/login", gs.Login) + // Create a group for our API + api := server.Group("/api") - // Every route from here on will require a valid JWT + // Register our unprotected routes + api.Post("/register", users.Register) + api.Post("/login", users.Login) + + // Every route from here on will require a valid + // JWT bearer token authentication in the header server.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) - // Protected routes (require a valid JWT bearer token authentication header) - server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport) - server.Get("/api/getUserProjects", 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 - server.Post("/api/project", gs.CreateProject) // WIP - server.Get("/api/project/:projectId", gs.GetProject) - server.Get("/api/project/getAllUsers", gs.GetAllUsersProject) - server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) - server.Get("/api/getUnsignedReports/:projectName", gs.GetUnsignedReports) - server.Post("/api/signReport", gs.SignReport) - server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) - server.Put("/api/changeUserName", gs.ChangeUserName) - server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) - server.Get("/api/users/all", gs.ListAllUsers) - server.Get("/api/getWeeklyReportsUser/:projectName", gs.GetWeeklyReportsUserHandler) - server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler) - server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange) - server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject) - server.Put("/api/updateWeeklyReport", gs.UpdateWeeklyReport) - server.Delete("/api/removeProject/:projectName", gs.RemoveProject) + // All user related routes + // userGroup := api.Group("/user") // Not currently in use + api.Post("/login", users.Login) + api.Post("/register", users.Register) + api.Post("/loginrenew", users.LoginRenew) + api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches + api.Put("/changeUserName", users.ChangeUserName) + api.Get("/users/all", users.ListAllUsers) + api.Post("/promoteToAdmin", users.PromoteToAdmin) + api.Get("/project/getAllUsers", users.GetAllUsersProject) + + // All project related routes + // projectGroup := api.Group("/project") // Not currently in use + api.Get("/getUserProjects", projects.GetUserProjects) + api.Delete("/project/:projectID", projects.DeleteProject) + api.Post("/project", projects.CreateProject) + api.Get("/project/:projectId", projects.GetProject) + api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) + api.Post("/ProjectRoleChange", projects.ProjectRoleChange) + api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) + api.Delete("/removeProject/:projectName", projects.RemoveProject) + + // All report related routes + // reportGroup := api.Group("/report") // Not currently in use + api.Get("/getWeeklyReport", reports.GetWeeklyReport) + api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) + api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) + api.Post("/signReport", reports.SignReport) + api.Put("/addUserToProject", projects.AddUserToProjectHandler) + api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) + api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) From 1d5fcd61b6e11712731561a7344df550e00e18b2 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 14:50:56 +0100 Subject: [PATCH 004/114] Refactor, report related endpoints now reside in individual files --- .../handlers/reports/GetUnsignedReports.go | 45 ++++ .../handlers/reports/GetWeeklyReport.go | 47 ++++ .../reports/GetWeeklyReportsUserHandler.go | 36 +++ .../internal/handlers/reports/SignReport.go | 48 ++++ .../handlers/reports/SubmitWeeklyReport.go | 41 ++++ .../handlers/reports/UpdateWeeklyReport.go | 44 ++++ .../reports/handlers_report_related.go | 214 ------------------ 7 files changed, 261 insertions(+), 214 deletions(-) create mode 100644 backend/internal/handlers/reports/GetUnsignedReports.go create mode 100644 backend/internal/handlers/reports/GetWeeklyReport.go create mode 100644 backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go create mode 100644 backend/internal/handlers/reports/SignReport.go create mode 100644 backend/internal/handlers/reports/SubmitWeeklyReport.go create mode 100644 backend/internal/handlers/reports/UpdateWeeklyReport.go delete mode 100644 backend/internal/handlers/reports/handlers_report_related.go diff --git a/backend/internal/handlers/reports/GetUnsignedReports.go b/backend/internal/handlers/reports/GetUnsignedReports.go new file mode 100644 index 0000000..9525f55 --- /dev/null +++ b/backend/internal/handlers/reports/GetUnsignedReports.go @@ -0,0 +1,45 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func GetUnsignedReports(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + projectManagerUsername := claims["name"].(string) + + // Extract project name and week from query parameters + projectName := c.Params("projectName") + + log.Info("Getting unsigned reports for") + + if projectName == "" { + log.Info("Missing project name") + return c.Status(400).SendString("Missing project name") + } + + // Get the project manager's ID + isProjectManager, err := db.GetDb(c).IsProjectManager(projectManagerUsername, projectName) + if err != nil { + log.Info("Failed to get project manager ID") + return c.Status(500).SendString("Failed to get project manager ID") + } + log.Info("User is Project Manager: ", isProjectManager) + + // Call the database function to get the unsigned weekly reports + reports, err := db.GetDb(c).GetUnsignedWeeklyReports(projectName) + if err != nil { + log.Info("Error getting unsigned weekly reports:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning unsigned reports") + // Return the list of unsigned reports + return c.JSON(reports) +} diff --git a/backend/internal/handlers/reports/GetWeeklyReport.go b/backend/internal/handlers/reports/GetWeeklyReport.go new file mode 100644 index 0000000..422bc0b --- /dev/null +++ b/backend/internal/handlers/reports/GetWeeklyReport.go @@ -0,0 +1,47 @@ +package reports + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// Handler for retrieving weekly report +func GetWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + log.Info("Getting weekly report for: ", username) + + // Extract project name and week from query parameters + projectName := c.Query("projectName") + week := c.Query("week") + + if projectName == "" || week == "" { + log.Info("Missing project name or week number") + return c.Status(400).SendString("Missing project name or week number") + } + + // Convert week to integer + weekInt, err := strconv.Atoi(week) + if err != nil { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + + // Call the database function to get the weekly report + report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt) + if err != nil { + log.Info("Error getting weekly report from db:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning weekly report") + // Return the retrieved weekly report + return c.JSON(report) +} diff --git a/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go b/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go new file mode 100644 index 0000000..da8a90b --- /dev/null +++ b/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go @@ -0,0 +1,36 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project +func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract necessary (path) parameters from the request + projectName := c.Params("projectName") + + // TODO: Here we need to check whether the user is a member of the project + // If not, we should return an error. On the other hand, if the user not a member, + // the returned list of reports will (should) allways be empty. + + // Retrieve weekly reports for the user in the project from the database + reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName) + if err != nil { + log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning weekly reports for user:", username, "in project:", projectName) + + // Return the list of reports as JSON + return c.JSON(reports) +} diff --git a/backend/internal/handlers/reports/SignReport.go b/backend/internal/handlers/reports/SignReport.go new file mode 100644 index 0000000..5769caf --- /dev/null +++ b/backend/internal/handlers/reports/SignReport.go @@ -0,0 +1,48 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// TODO: This information should be extracted from path parameters +type ReportId struct { + ReportId int +} + +func SignReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + projectManagerUsername := claims["name"].(string) + + log.Info("Signing report for: ", projectManagerUsername) + + // Extract report ID from the request query parameters + // reportID := c.Query("reportId") + rid := new(ReportId) + if err := c.BodyParser(rid); err != nil { + return err + } + log.Info("Signing report for: ", rid.ReportId) + + // Get the project manager's ID + projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) + if err != nil { + log.Info("Failed to get project manager ID") + return c.Status(500).SendString("Failed to get project manager ID") + } + log.Info("Project manager ID: ", projectManagerID) + + // Call the database function to sign the weekly report + err = db.GetDb(c).SignWeeklyReport(rid.ReportId, projectManagerID) + if err != nil { + log.Info("Error signing weekly report:", err) + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Weekly report signed successfully") +} diff --git a/backend/internal/handlers/reports/SubmitWeeklyReport.go b/backend/internal/handlers/reports/SubmitWeeklyReport.go new file mode 100644 index 0000000..900aa03 --- /dev/null +++ b/backend/internal/handlers/reports/SubmitWeeklyReport.go @@ -0,0 +1,41 @@ +package reports + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func SubmitWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + report := new(types.NewWeeklyReport) + if err := c.BodyParser(report); err != nil { + log.Info("Error parsing weekly report") + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if report.Week < 1 || report.Week > 52 { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { + log.Info("Invalid time report") + return c.Status(400).SendString("Invalid time report") + } + + if err := db.GetDb(c).AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { + log.Info("Error adding weekly report to db:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Weekly report added") + return c.Status(200).SendString("Time report added") +} diff --git a/backend/internal/handlers/reports/UpdateWeeklyReport.go b/backend/internal/handlers/reports/UpdateWeeklyReport.go new file mode 100644 index 0000000..3ab835d --- /dev/null +++ b/backend/internal/handlers/reports/UpdateWeeklyReport.go @@ -0,0 +1,44 @@ +package reports + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func UpdateWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Parse the request body into an UpdateWeeklyReport struct + var updateReport types.UpdateWeeklyReport + if err := c.BodyParser(&updateReport); err != nil { + log.Info("Error parsing weekly report") + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if updateReport.Week < 1 || updateReport.Week > 52 { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + + if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 { + log.Info("Invalid time report") + return c.Status(400).SendString("Invalid time report") + } + + // Update the weekly report in the database + if err := db.GetDb(c).UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { + log.Info("Error updating weekly report in db:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Weekly report updated") + return c.Status(200).SendString("Weekly report updated") +} diff --git a/backend/internal/handlers/reports/handlers_report_related.go b/backend/internal/handlers/reports/handlers_report_related.go deleted file mode 100644 index 1c84d52..0000000 --- a/backend/internal/handlers/reports/handlers_report_related.go +++ /dev/null @@ -1,214 +0,0 @@ -package reports - -import ( - "strconv" - db "ttime/internal/database" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -func SubmitWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - report := new(types.NewWeeklyReport) - if err := c.BodyParser(report); err != nil { - log.Info("Error parsing weekly report") - return c.Status(400).SendString(err.Error()) - } - - // Make sure all the fields of the report are valid - if report.Week < 1 || report.Week > 52 { - log.Info("Invalid week number") - return c.Status(400).SendString("Invalid week number") - } - if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { - log.Info("Invalid time report") - return c.Status(400).SendString("Invalid time report") - } - - if err := db.GetDb(c).AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { - log.Info("Error adding weekly report to db:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Weekly report added") - return c.Status(200).SendString("Time report added") -} - -// Handler for retrieving weekly report -func GetWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the request - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - log.Info("Getting weekly report for: ", username) - - // Extract project name and week from query parameters - projectName := c.Query("projectName") - week := c.Query("week") - - if projectName == "" || week == "" { - log.Info("Missing project name or week number") - return c.Status(400).SendString("Missing project name or week number") - } - - // Convert week to integer - weekInt, err := strconv.Atoi(week) - if err != nil { - log.Info("Invalid week number") - return c.Status(400).SendString("Invalid week number") - } - - // Call the database function to get the weekly report - report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt) - if err != nil { - log.Info("Error getting weekly report from db:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning weekly report") - // Return the retrieved weekly report - return c.JSON(report) -} - -type ReportId struct { - ReportId int -} - -func SignReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - projectManagerUsername := claims["name"].(string) - - log.Info("Signing report for: ", projectManagerUsername) - - // Extract report ID from the request query parameters - // reportID := c.Query("reportId") - rid := new(ReportId) - if err := c.BodyParser(rid); err != nil { - return err - } - log.Info("Signing report for: ", rid.ReportId) - - // Get the project manager's ID - projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) - if err != nil { - log.Info("Failed to get project manager ID") - return c.Status(500).SendString("Failed to get project manager ID") - } - log.Info("Project manager ID: ", projectManagerID) - - // Call the database function to sign the weekly report - err = db.GetDb(c).SignWeeklyReport(rid.ReportId, projectManagerID) - if err != nil { - log.Info("Error signing weekly report:", err) - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Weekly report signed successfully") -} - -func GetUnsignedReports(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - projectManagerUsername := claims["name"].(string) - - // Extract project name and week from query parameters - projectName := c.Params("projectName") - - log.Info("Getting unsigned reports for") - - if projectName == "" { - log.Info("Missing project name") - return c.Status(400).SendString("Missing project name") - } - - // Get the project manager's ID - isProjectManager, err := db.GetDb(c).IsProjectManager(projectManagerUsername, projectName) - if err != nil { - log.Info("Failed to get project manager ID") - return c.Status(500).SendString("Failed to get project manager ID") - } - log.Info("User is Project Manager: ", isProjectManager) - - // Call the database function to get the unsigned weekly reports - reports, err := db.GetDb(c).GetUnsignedWeeklyReports(projectName) - if err != nil { - log.Info("Error getting unsigned weekly reports:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning unsigned reports") - // Return the list of unsigned reports - return c.JSON(reports) -} - -// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project -func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract necessary (path) parameters from the request - projectName := c.Params("projectName") - - // TODO: Here we need to check whether the user is a member of the project - // If not, we should return an error. On the other hand, if the user not a member, - // the returned list of reports will (should) allways be empty. - - // Retrieve weekly reports for the user in the project from the database - reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName) - if err != nil { - log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning weekly reports for user:", username, "in project:", projectName) - - // Return the list of reports as JSON - return c.JSON(reports) -} - -func UpdateWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Parse the request body into an UpdateWeeklyReport struct - var updateReport types.UpdateWeeklyReport - if err := c.BodyParser(&updateReport); err != nil { - log.Info("Error parsing weekly report") - return c.Status(400).SendString(err.Error()) - } - - // Make sure all the fields of the report are valid - if updateReport.Week < 1 || updateReport.Week > 52 { - log.Info("Invalid week number") - return c.Status(400).SendString("Invalid week number") - } - - if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 { - log.Info("Invalid time report") - return c.Status(400).SendString("Invalid time report") - } - - // Update the weekly report in the database - if err := db.GetDb(c).UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { - log.Info("Error updating weekly report in db:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Weekly report updated") - return c.Status(200).SendString("Weekly report updated") -} From b927fb80fb7aa08b68bcccb8c1ca4fe474a7e905 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 15:00:29 +0100 Subject: [PATCH 005/114] Refactor again, splitting project related handlers --- .../handlers/projects/AddUserToProject.go | 51 +++ .../handlers/projects/CreateProject.go | 30 ++ .../handlers/projects/DeleteProject.go | 19 ++ .../internal/handlers/projects/GetProject.go | 38 +++ .../handlers/projects/GetProjectTimes.go | 63 ++++ .../handlers/projects/GetUserProject.go | 25 ++ .../handlers/projects/IsProjectManager.go | 32 ++ .../handlers/projects/ListAllUserProjects.go | 55 +++ .../handlers/projects/ProjectRoleChange.go | 45 +++ .../handlers/projects/RemoveProject.go | 35 ++ .../projects/handlers_project_related.go | 316 ------------------ 11 files changed, 393 insertions(+), 316 deletions(-) create mode 100644 backend/internal/handlers/projects/AddUserToProject.go create mode 100644 backend/internal/handlers/projects/CreateProject.go create mode 100644 backend/internal/handlers/projects/DeleteProject.go create mode 100644 backend/internal/handlers/projects/GetProject.go create mode 100644 backend/internal/handlers/projects/GetProjectTimes.go create mode 100644 backend/internal/handlers/projects/GetUserProject.go create mode 100644 backend/internal/handlers/projects/IsProjectManager.go create mode 100644 backend/internal/handlers/projects/ListAllUserProjects.go create mode 100644 backend/internal/handlers/projects/ProjectRoleChange.go create mode 100644 backend/internal/handlers/projects/RemoveProject.go delete mode 100644 backend/internal/handlers/projects/handlers_project_related.go diff --git a/backend/internal/handlers/projects/AddUserToProject.go b/backend/internal/handlers/projects/AddUserToProject.go new file mode 100644 index 0000000..702b7dd --- /dev/null +++ b/backend/internal/handlers/projects/AddUserToProject.go @@ -0,0 +1,51 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// AddUserToProjectHandler is a handler that adds a user to a project with a specified role +func AddUserToProjectHandler(c *fiber.Ctx) error { + // Extract necessary parameters from the request + var requestData struct { + Username string `json:"username"` + ProjectName string `json:"projectName"` + Role string `json:"role"` + } + if err := c.BodyParser(&requestData); err != nil { + log.Info("Error parsing request body:", err) + return c.Status(400).SendString("Bad request") + } + + // Check if the user adding another user to the project is a site admin + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info("Admin username from claims:", adminUsername) + + isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + log.Info("User is not a site admin:", adminUsername) + return c.Status(403).SendString("User is not a site admin") + } + + // Add the user to the project with the specified role + err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + if err != nil { + log.Info("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/projects/CreateProject.go b/backend/internal/handlers/projects/CreateProject.go new file mode 100644 index 0000000..cef2f2b --- /dev/null +++ b/backend/internal/handlers/projects/CreateProject.go @@ -0,0 +1,30 @@ +package projects + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// CreateProject is a simple handler that creates a new project +func CreateProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + p := new(types.NewProject) + if err := c.BodyParser(p); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Get the username from the token and set it as the owner of the project + // This is ugly but + claims := user.Claims.(jwt.MapClaims) + owner := claims["name"].(string) + + if err := db.GetDb(c).AddProject(p.Name, p.Description, owner); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Project added") +} diff --git a/backend/internal/handlers/projects/DeleteProject.go b/backend/internal/handlers/projects/DeleteProject.go new file mode 100644 index 0000000..415424a --- /dev/null +++ b/backend/internal/handlers/projects/DeleteProject.go @@ -0,0 +1,19 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" +) + +func DeleteProject(c *fiber.Ctx) error { + + projectID := c.Params("projectID") + username := c.Params("username") + + if err := db.GetDb(c).DeleteProject(projectID, username); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} diff --git a/backend/internal/handlers/projects/GetProject.go b/backend/internal/handlers/projects/GetProject.go new file mode 100644 index 0000000..03333ce --- /dev/null +++ b/backend/internal/handlers/projects/GetProject.go @@ -0,0 +1,38 @@ +package projects + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// GetProject retrieves a specific project by its ID +func GetProject(c *fiber.Ctx) error { + // Extract the project ID from the request parameters or body + projectID := c.Params("projectID") + if projectID == "" { + log.Info("No project ID provided") + return c.Status(400).SendString("No project ID provided") + } + log.Info("Getting project with ID: ", projectID) + + // Parse the project ID into an integer + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + log.Info("Invalid project ID") + return c.Status(400).SendString("Invalid project ID") + } + + // Get the project from the database by its ID + project, err := db.GetDb(c).GetProject(projectIDInt) + if err != nil { + log.Info("Error getting project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return the project as JSON + log.Info("Returning project: ", project.Name) + return c.JSON(project) +} diff --git a/backend/internal/handlers/projects/GetProjectTimes.go b/backend/internal/handlers/projects/GetProjectTimes.go new file mode 100644 index 0000000..573a95e --- /dev/null +++ b/backend/internal/handlers/projects/GetProjectTimes.go @@ -0,0 +1,63 @@ +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 GetProjectTimesHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Get project + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get all users in the project and roles + userProjects, err := db.GetDb(c).GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users in project:", err) + return c.Status(500).SendString(err.Error()) + } + + // If the user is member + isMember := false + for _, userProject := range userProjects { + if userProject.Username == username { + isMember = true + break + } + } + + // If the user is admin + if !isMember { + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project member nor a site admin:", username) + return c.Status(403).SendString("User is neither a project member nor a site admin") + } + } + + // Get project times + projectTimes, err := db.GetDb(c).GetProjectTimes(projectName) + if err != nil { + log.Info("Error getting project times:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return project times as JSON + log.Info("Returning project times for project:", projectName) + return c.JSON(projectTimes) +} diff --git a/backend/internal/handlers/projects/GetUserProject.go b/backend/internal/handlers/projects/GetUserProject.go new file mode 100644 index 0000000..99ed63b --- /dev/null +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -0,0 +1,25 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// GetUserProjects returns all projects that the user is a member of +func 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) + + // Then dip into the database to get the projects + projects, err := db.GetDb(c).GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} diff --git a/backend/internal/handlers/projects/IsProjectManager.go b/backend/internal/handlers/projects/IsProjectManager.go new file mode 100644 index 0000000..678fad5 --- /dev/null +++ b/backend/internal/handlers/projects/IsProjectManager.go @@ -0,0 +1,32 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project +func IsProjectManagerHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract necessary parameters from the request query string + projectName := c.Params("projectName") + + log.Info("Checking if user ", username, " is a project manager for project ", projectName) + + // Check if the user is a project manager for the specified project + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking project manager status:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return the result as JSON + return c.JSON(fiber.Map{"isProjectManager": isManager}) +} diff --git a/backend/internal/handlers/projects/ListAllUserProjects.go b/backend/internal/handlers/projects/ListAllUserProjects.go new file mode 100644 index 0000000..e0bcaf5 --- /dev/null +++ b/backend/internal/handlers/projects/ListAllUserProjects.go @@ -0,0 +1,55 @@ +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 ListAllUsersProject(c *fiber.Ctx) error { + // Extract the project name from the request parameters or body + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get the user token + userToken := c.Locals("user").(*jwt.Token) + claims := userToken.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a project manager for the specified project + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking project manager status:", err) + return c.Status(500).SendString(err.Error()) + } + + // If the user is not a project manager, check if the user is a site admin + if !isManager { + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project manager nor a site admin:", username) + return c.Status(403).SendString("User is neither a project manager nor a site admin") + } + } + + // Get all users associated with the project from the database + users, err := db.GetDb(c).GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users for project:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning users for project: ", projectName) + + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/projects/ProjectRoleChange.go b/backend/internal/handlers/projects/ProjectRoleChange.go new file mode 100644 index 0000000..266127d --- /dev/null +++ b/backend/internal/handlers/projects/ProjectRoleChange.go @@ -0,0 +1,45 @@ +package projects + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ProjectRoleChange is a handler that changes a user's role within a project +func ProjectRoleChange(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 + data := new(types.RoleChange) + if err := c.BodyParser(data); err != nil { + log.Info("error parsing username, project or role") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) + + // Dubble diping and checcking if current user is + if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); 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") + } + + // Change the user's role within the project in the database + if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/projects/RemoveProject.go b/backend/internal/handlers/projects/RemoveProject.go new file mode 100644 index 0000000..7b140dd --- /dev/null +++ b/backend/internal/handlers/projects/RemoveProject.go @@ -0,0 +1,35 @@ +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 RemoveProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a site admin + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + log.Info("User is not a site admin:", username) + return c.Status(403).SendString("User is not a site admin") + } + + projectName := c.Params("projectName") + + if err := db.GetDb(c).RemoveProject(projectName); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} diff --git a/backend/internal/handlers/projects/handlers_project_related.go b/backend/internal/handlers/projects/handlers_project_related.go deleted file mode 100644 index 3429504..0000000 --- a/backend/internal/handlers/projects/handlers_project_related.go +++ /dev/null @@ -1,316 +0,0 @@ -package projects - -import ( - "strconv" - db "ttime/internal/database" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -// CreateProject is a simple handler that creates a new project -func CreateProject(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - p := new(types.NewProject) - if err := c.BodyParser(p); err != nil { - return c.Status(400).SendString(err.Error()) - } - - // Get the username from the token and set it as the owner of the project - // This is ugly but - claims := user.Claims.(jwt.MapClaims) - owner := claims["name"].(string) - - if err := db.GetDb(c).AddProject(p.Name, p.Description, owner); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Project added") -} - -func DeleteProject(c *fiber.Ctx) error { - - projectID := c.Params("projectID") - username := c.Params("username") - - if err := db.GetDb(c).DeleteProject(projectID, username); err != nil { - return c.Status(500).SendString((err.Error())) - } - - return c.Status(200).SendString("Project deleted") -} - -// GetUserProjects returns all projects that the user is a member of -func 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) - - // Then dip into the database to get the projects - projects, err := db.GetDb(c).GetProjectsForUser(username) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a json serialized list of projects - return c.JSON(projects) -} - -// ProjectRoleChange is a handler that changes a user's role within a project -func ProjectRoleChange(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 - data := new(types.RoleChange) - if err := c.BodyParser(data); err != nil { - log.Info("error parsing username, project or role") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) - - // Dubble diping and checcking if current user is - if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); 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") - } - - // Change the user's role within the project in the database - if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} - -// GetProject retrieves a specific project by its ID -func GetProject(c *fiber.Ctx) error { - // Extract the project ID from the request parameters or body - projectID := c.Params("projectID") - if projectID == "" { - log.Info("No project ID provided") - return c.Status(400).SendString("No project ID provided") - } - log.Info("Getting project with ID: ", projectID) - - // Parse the project ID into an integer - projectIDInt, err := strconv.Atoi(projectID) - if err != nil { - log.Info("Invalid project ID") - return c.Status(400).SendString("Invalid project ID") - } - - // Get the project from the database by its ID - project, err := db.GetDb(c).GetProject(projectIDInt) - if err != nil { - log.Info("Error getting project:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return the project as JSON - log.Info("Returning project: ", project.Name) - return c.JSON(project) -} - -func ListAllUsersProject(c *fiber.Ctx) error { - // Extract the project name from the request parameters or body - projectName := c.Params("projectName") - if projectName == "" { - log.Info("No project name provided") - return c.Status(400).SendString("No project name provided") - } - - // Get the user token - userToken := c.Locals("user").(*jwt.Token) - claims := userToken.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Check if the user is a project manager for the specified project - isManager, err := db.GetDb(c).IsProjectManager(username, projectName) - if err != nil { - log.Info("Error checking project manager status:", err) - return c.Status(500).SendString(err.Error()) - } - - // If the user is not a project manager, check if the user is a site admin - if !isManager { - isAdmin, err := db.GetDb(c).IsSiteAdmin(username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - if !isAdmin { - log.Info("User is neither a project manager nor a site admin:", username) - return c.Status(403).SendString("User is neither a project manager nor a site admin") - } - } - - // Get all users associated with the project from the database - users, err := db.GetDb(c).GetAllUsersProject(projectName) - if err != nil { - log.Info("Error getting users for project:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning users for project: ", projectName) - - // Return the list of users as JSON - return c.JSON(users) -} - -// AddUserToProjectHandler is a handler that adds a user to a project with a specified role -func AddUserToProjectHandler(c *fiber.Ctx) error { - // Extract necessary parameters from the request - var requestData struct { - Username string `json:"username"` - ProjectName string `json:"projectName"` - Role string `json:"role"` - } - if err := c.BodyParser(&requestData); err != nil { - log.Info("Error parsing request body:", err) - return c.Status(400).SendString("Bad request") - } - - // Check if the user adding another user to the project is a site admin - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - adminUsername := claims["name"].(string) - log.Info("Admin username from claims:", adminUsername) - - isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - - if !isAdmin { - log.Info("User is not a site admin:", adminUsername) - return c.Status(403).SendString("User is not a site admin") - } - - // Add the user to the project with the specified role - err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) - if err != nil { - log.Info("Error adding user to project:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return success message - log.Info("User added to project successfully:", requestData.Username) - return c.SendStatus(fiber.StatusOK) -} - -// IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project -func IsProjectManagerHandler(c *fiber.Ctx) error { - // Get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract necessary parameters from the request query string - projectName := c.Params("projectName") - - log.Info("Checking if user ", username, " is a project manager for project ", projectName) - - // Check if the user is a project manager for the specified project - isManager, err := db.GetDb(c).IsProjectManager(username, projectName) - if err != nil { - log.Info("Error checking project manager status:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return the result as JSON - return c.JSON(fiber.Map{"isProjectManager": isManager}) -} - -func GetProjectTimesHandler(c *fiber.Ctx) error { - // Get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Get project - projectName := c.Params("projectName") - if projectName == "" { - log.Info("No project name provided") - return c.Status(400).SendString("No project name provided") - } - - // Get all users in the project and roles - userProjects, err := db.GetDb(c).GetAllUsersProject(projectName) - if err != nil { - log.Info("Error getting users in project:", err) - return c.Status(500).SendString(err.Error()) - } - - // If the user is member - isMember := false - for _, userProject := range userProjects { - if userProject.Username == username { - isMember = true - break - } - } - - // If the user is admin - if !isMember { - isAdmin, err := db.GetDb(c).IsSiteAdmin(username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - if !isAdmin { - log.Info("User is neither a project member nor a site admin:", username) - return c.Status(403).SendString("User is neither a project member nor a site admin") - } - } - - // Get project times - projectTimes, err := db.GetDb(c).GetProjectTimes(projectName) - if err != nil { - log.Info("Error getting project times:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return project times as JSON - log.Info("Returning project times for project:", projectName) - return c.JSON(projectTimes) -} - -func RemoveProject(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Check if the user is a site admin - isAdmin, err := db.GetDb(c).IsSiteAdmin(username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - - if !isAdmin { - log.Info("User is not a site admin:", username) - return c.Status(403).SendString("User is not a site admin") - } - - projectName := c.Params("projectName") - - if err := db.GetDb(c).RemoveProject(projectName); err != nil { - return c.Status(500).SendString((err.Error())) - } - - return c.Status(200).SendString("Project deleted") -} From 4538a3b1938b6df923d3f96907301870ffafcba0 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 15:33:20 +0100 Subject: [PATCH 006/114] SignReport handler changes along with tests and TS interface --- .../internal/handlers/reports/SignReport.go | 25 +++++-------- backend/main.go | 2 +- frontend/src/API/API.ts | 35 +++++++++++++++++++ testing.py | 5 ++- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/backend/internal/handlers/reports/SignReport.go b/backend/internal/handlers/reports/SignReport.go index 5769caf..a486ecc 100644 --- a/backend/internal/handlers/reports/SignReport.go +++ b/backend/internal/handlers/reports/SignReport.go @@ -1,6 +1,7 @@ package reports import ( + "strconv" db "ttime/internal/database" "github.com/gofiber/fiber/v2" @@ -8,41 +9,33 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// TODO: This information should be extracted from path parameters -type ReportId struct { - ReportId int -} - func SignReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) projectManagerUsername := claims["name"].(string) - log.Info("Signing report for: ", projectManagerUsername) - - // Extract report ID from the request query parameters - // reportID := c.Query("reportId") - rid := new(ReportId) - if err := c.BodyParser(rid); err != nil { - return err + // Extract report ID from the path + reportId, err := strconv.Atoi(c.Params("reportId")) + if err != nil { + log.Info("Invalid report ID") + return c.Status(400).SendString("Invalid report ID") } - log.Info("Signing report for: ", rid.ReportId) // Get the project manager's ID projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) if err != nil { - log.Info("Failed to get project manager ID") + log.Info("Failed to get project manager ID for user: ", projectManagerUsername) return c.Status(500).SendString("Failed to get project manager ID") } - log.Info("Project manager ID: ", projectManagerID) // Call the database function to sign the weekly report - err = db.GetDb(c).SignWeeklyReport(rid.ReportId, projectManagerID) + err = db.GetDb(c).SignWeeklyReport(reportId, projectManagerID) if err != nil { log.Info("Error signing weekly report:", err) return c.Status(500).SendString(err.Error()) } + log.Info("Project manager ID: ", projectManagerID, " signed report ID: ", reportId) return c.Status(200).SendString("Weekly report signed successfully") } diff --git a/backend/main.go b/backend/main.go index ebe5660..cae088f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -125,7 +125,7 @@ func main() { api.Get("/getWeeklyReport", reports.GetWeeklyReport) api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) - api.Post("/signReport", reports.SignReport) + api.Put("/signReport/:reportId", reports.SignReport) api.Put("/addUserToProject", projects.AddUserToProjectHandler) api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 0160e15..886c957 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -153,6 +153,18 @@ interface API { projectName: string, token: string, ): Promise>; + + /** + * Signs a report. Keep in mind that the user which the token belongs to must be + * the project manager of the project the report belongs to. + * + * @param {number} reportId The id of the report to sign + * @param {string} token The authentication token + */ + signReport( + reportId: number, + token: string, + ): Promise>; } /** An instance of the API */ @@ -581,4 +593,27 @@ export const api: API = { }); } }, + + async signReport( + reportId: number, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/signReport/${reportId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { success: false, message: "Failed to sign report" }; + } else { + return { success: true, message: "Report signed" }; + } + } catch (e) { + return { success: false, message: "Failed to sign report" }; + } + } }; diff --git a/testing.py b/testing.py index 5fb8ffe..d4594d1 100644 --- a/testing.py +++ b/testing.py @@ -301,9 +301,8 @@ def test_sign_report(): report_id = response.json()["reportId"] # Sign the report as the project manager - response = requests.post( - signReportPath, - json={"reportId": report_id}, + response = requests.put( + signReportPath + "/" + str(report_id), headers={"Authorization": "Bearer " + project_manager_token}, ) assert response.status_code == 200, "Sign report failed" From 0cd0c8d8326a131aec98c95e84a3b47b794bdf7f Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 15:36:42 +0100 Subject: [PATCH 007/114] Handler re-order to satisfy human OCD --- backend/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/backend/main.go b/backend/main.go index cae088f..4c2056e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -100,34 +100,34 @@ func main() { // All user related routes // userGroup := api.Group("/user") // Not currently in use + api.Get("/users/all", users.ListAllUsers) + api.Get("/project/getAllUsers", users.GetAllUsersProject) api.Post("/login", users.Login) api.Post("/register", users.Register) api.Post("/loginrenew", users.LoginRenew) - api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches - api.Put("/changeUserName", users.ChangeUserName) - api.Get("/users/all", users.ListAllUsers) api.Post("/promoteToAdmin", users.PromoteToAdmin) - api.Get("/project/getAllUsers", users.GetAllUsersProject) + api.Put("/changeUserName", users.ChangeUserName) + api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches // All project related routes // projectGroup := api.Group("/project") // Not currently in use api.Get("/getUserProjects", projects.GetUserProjects) - api.Delete("/project/:projectID", projects.DeleteProject) - api.Post("/project", projects.CreateProject) api.Get("/project/:projectId", projects.GetProject) api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) - api.Post("/ProjectRoleChange", projects.ProjectRoleChange) api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) + api.Post("/project", projects.CreateProject) + api.Post("/ProjectRoleChange", projects.ProjectRoleChange) api.Delete("/removeProject/:projectName", projects.RemoveProject) + api.Delete("/project/:projectID", projects.DeleteProject) // All report related routes // reportGroup := api.Group("/report") // Not currently in use api.Get("/getWeeklyReport", reports.GetWeeklyReport) - api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) + api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) + api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) api.Put("/signReport/:reportId", reports.SignReport) api.Put("/addUserToProject", projects.AddUserToProjectHandler) - api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server From 1db1b84e8f9ee514f4c8e30fe35ac942e380d75e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 15:58:36 +0100 Subject: [PATCH 008/114] Initial demo of swagger-typescript-api interface generation --- .gitignore | 1 + backend/Makefile | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index 3b1c6d3..281e866 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ backend/*.svg /go.work.sum /package-lock.json +/backend/docs/swagger.json # Test binary, built with `go test -c` *.test diff --git a/backend/Makefile b/backend/Makefile index 3443e94..4e6e20e 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -104,6 +104,12 @@ default: build docs: swag init -outputTypes go +api: ./docs/swagger.json + npx swagger-typescript-api --path ./docs/swagger.json --output ../frontend/src/API --name GAPI.ts + +./docs/swagger.json: + swag init -outputTypes json + .PHONY: docfmt docfmt: swag fmt From 45e891ed2ccef542dceac43fa1d9bd3eafb594c1 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 15:59:36 +0100 Subject: [PATCH 009/114] Regenerated swagger docs --- backend/docs/docs.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 322c812..0009c17 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -137,13 +137,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully prometed user", + "description": "Successfully promoted user", "schema": { "type": "json" } }, "400": { - "description": "bad request", + "description": "Bad request", "schema": { "type": "string" } From 0792c6b8a3160f9096468438ce867389dac9cf37 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 16:36:47 +0100 Subject: [PATCH 010/114] More sane swagger-typescript-api generator parameters --- backend/Makefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/Makefile b/backend/Makefile index 4e6e20e..039340c 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -105,7 +105,11 @@ docs: swag init -outputTypes go api: ./docs/swagger.json - npx swagger-typescript-api --path ./docs/swagger.json --output ../frontend/src/API --name GAPI.ts + npx swagger-typescript-api \ + --api-class-name GenApi \ + --path ./docs/swagger.json \ + --output ../frontend/src/API \ + --name GenApi.ts \ ./docs/swagger.json: swag init -outputTypes json From 374e357820f2bc02069b4d2917b1c4dd7cd68247 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 16:41:55 +0100 Subject: [PATCH 011/114] Add database.txt to clean target --- backend/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/Makefile b/backend/Makefile index 039340c..41fe1a3 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -34,6 +34,7 @@ clean: rm -f plantuml.jar rm -f erd.png rm -f config.toml + rm -f database.txt # Test target test: db.sqlite3 From 05545f6f886d45c198af23fd0e4a1d0ce693886b Mon Sep 17 00:00:00 2001 From: Davenludd Date: Fri, 29 Mar 2024 17:53:37 +0100 Subject: [PATCH 012/114] Minor fixes --- frontend/src/Components/DisplayUserProjects.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index 29e4bcb..494e6b0 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -25,8 +25,9 @@ function DisplayUserProject(): JSX.Element { const handleProjectClick = async (projectName: string): Promise => { const token = localStorage.getItem("accessToken") ?? ""; const response = await api.checkIfProjectManager(projectName, token); + console.log(response.data); if (response.success) { - if (response.data) { + if (response.data === true) { navigate(`/PMProjectPage/${projectName}`); } else { navigate(`/project/${projectName}`); From c1f49915baff3271ded8d5b33bf8f57b52715db7 Mon Sep 17 00:00:00 2001 From: Davenludd Date: Fri, 29 Mar 2024 18:29:38 +0100 Subject: [PATCH 013/114] Refactor signReport method signature --- frontend/src/API/API.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 886c957..0a85e70 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -161,10 +161,7 @@ interface API { * @param {number} reportId The id of the report to sign * @param {string} token The authentication token */ - signReport( - reportId: number, - token: string, - ): Promise>; + signReport(reportId: number, token: string): Promise>; } /** An instance of the API */ @@ -615,5 +612,5 @@ export const api: API = { } catch (e) { return { success: false, message: "Failed to sign report" }; } - } + }, }; From 1385011769b1e36bed8a352b6508f34b2841df4e Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 18:41:20 +0100 Subject: [PATCH 014/114] Make swagger-typescript-api makefile target wipe the previous version --- backend/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/Makefile b/backend/Makefile index 41fe1a3..0ffc557 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -106,6 +106,7 @@ docs: swag init -outputTypes go api: ./docs/swagger.json + rm ../frontend/src/API/GenApi.ts npx swagger-typescript-api \ --api-class-name GenApi \ --path ./docs/swagger.json \ From c2fa9aa0c1bf62e9fb1418072610b63558642d9c Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 18:42:12 +0100 Subject: [PATCH 015/114] Lots of fiddling with swagger annotations in user related handlers --- .../internal/handlers/users/ListAllUsers.go | 21 +++++------ backend/internal/handlers/users/Login.go | 25 ++++++------- backend/internal/handlers/users/LoginRenew.go | 36 +++++++++++-------- .../internal/handlers/users/PromoteToAdmin.go | 25 +++++++------ backend/internal/handlers/users/Register.go | 6 ++-- backend/internal/handlers/users/UserDelete.go | 6 ++-- backend/internal/types/users.go | 4 +-- 7 files changed, 67 insertions(+), 56 deletions(-) diff --git a/backend/internal/handlers/users/ListAllUsers.go b/backend/internal/handlers/users/ListAllUsers.go index 1cae76c..5ac5df0 100644 --- a/backend/internal/handlers/users/ListAllUsers.go +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -7,16 +7,17 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// ListAllUsers is a handler that returns a list of all users in the application database -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {json} json "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Produce json +// @Security JWT +// @Success 200 {array} string "Successfully returned all users" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] +// +// ListAllUsers returns a list of all users in the application database func ListAllUsers(c *fiber.Ctx) error { // Get all users from the database users, err := db.GetDb(c).GetAllUsersApplication() diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go index c4d6c60..42c52a5 100644 --- a/backend/internal/handlers/users/Login.go +++ b/backend/internal/handlers/users/Login.go @@ -10,18 +10,19 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// Login is a simple login handler that returns a JWT token -// @Summary login -// @Description logs the user in and returns a jwt token -// @Tags User -// @Accept json -// @Param NewUser body types.NewUser true "login info" -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] +// @Summary Login +// @Description Logs in a user and returns a JWT token +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body types.NewUser true "User credentials" +// @Success 200 {object} types.Token "JWT token" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] +// +// Login logs in a user and returns a JWT token func Login(c *fiber.Ctx) error { // The body type is identical to a NewUser diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go index 78eadfd..3926ce4 100644 --- a/backend/internal/handlers/users/LoginRenew.go +++ b/backend/internal/handlers/users/LoginRenew.go @@ -9,34 +9,40 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// LoginRenew is a simple handler that renews the token -// @Summary LoginRenews -// @Description renews the users token -// @Security bererToken -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginerenew [post] +// @Summary LoginRenews +// @Description Renews the users token. +// @Tags Auth +// @Produce json +// @Security JWT +// @Success 200 {object} types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginrenew [post] +// +// LoginRenew renews the users token func LoginRenew(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) + // Renewing the token means we trust whatever is already in the token claims := user.Claims.(jwt.MapClaims) + + // 72 hour expiration time claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - renewed := jwt.MapClaims{ + + // Create token with old claims, but new expiration time + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "name": claims["name"], "admin": claims["admin"], "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + }) + + // Sign it with top secret key t, err := token.SignedString([]byte("secret")) if err != nil { log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) + return c.SendStatus(fiber.StatusInternalServerError) // 500 } log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go index 4a21758..3f0a6d3 100644 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -8,17 +8,20 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @Summary PromoteToAdmin -// @Description promote chosen user to admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] +// @Summary PromoteToAdmin +// @Description Promote chosen user to site admin +// @Tags User +// @Accept json +// @Produce plain +// @Security JWT +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {object} types.Token "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] +// +// PromoteToAdmin promotes a user to a site admin func PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go index 9977246..b9e0c78 100644 --- a/backend/internal/handlers/users/Register.go +++ b/backend/internal/handlers/users/Register.go @@ -8,11 +8,9 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// Register is a simple handler that registers a new user -// // @Summary Register // @Description Register a new user -// @Tags User +// @Tags Auth // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "User to register" @@ -20,6 +18,8 @@ import ( // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /register [post] +// +// Register is a simple handler that registers a new user func Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go index 5957c2d..491a1b3 100644 --- a/backend/internal/handlers/users/UserDelete.go +++ b/backend/internal/handlers/users/UserDelete.go @@ -8,19 +8,19 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -// // @Summary UserDelete // @Description UserDelete deletes a user from the database // @Tags User // @Accept json // @Produce plain +// @Security JWT // @Success 200 {string} string "User deleted" // @Failure 403 {string} string "You can only delete yourself" // @Failure 500 {string} string "Internal server error" // @Failure 401 {string} string "Unauthorized" // @Router /userdelete/{username} [delete] +// +// UserDelete deletes a user from the database func UserDelete(c *fiber.Ctx) error { // Read from path parameters username := c.Params("username") diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 88b4f06..37cc8c2 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) { // Should be used when registering, for example type NewUser struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username" example:"username123"` + Password string `json:"password" example:"password123"` } // PublicUser represents a user that is safe to send over the API (no password) From 87a19bfd4e50cc9e19fe2a232a554ee0e3b70685 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 18:42:53 +0100 Subject: [PATCH 016/114] Swagger annotations for JWT key --- backend/main.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/backend/main.go b/backend/main.go index 4c2056e..cf58280 100644 --- a/backend/main.go +++ b/backend/main.go @@ -25,15 +25,16 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -//@securityDefinitions.apikey bererToken -//@in header -//@name Authorization +// @securityDefinitions.apikey JWT +// @in header +// @name Authorization +// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".** // @host localhost:8080 // @BasePath /api -// @externalDocs.description OpenAPI -// @externalDocs.url https://swagger.io/resources/open-api/ +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ /** Main function for starting the server and initializing configurations. From f1e15137d687b4c4b47e64d5bf01db7dfcd7d83c Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 18:43:26 +0100 Subject: [PATCH 017/114] Freshly generated swagger docs --- backend/docs/docs.go | 79 ++++++++++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 0009c17..7a08b0e 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "logs the user in and returns a jwt token", + "description": "Logs in a user and returns a JWT token", "consumes": [ "application/json" ], "produces": [ - "text/plain" + "application/json" ], "tags": [ - "User" + "Auth" ], - "summary": "login", + "summary": "Login", "parameters": [ { - "description": "login info", - "name": "NewUser", + "description": "User credentials", + "name": "body", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully signed token for user", + "description": "JWT token", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "400": { @@ -71,29 +71,26 @@ const docTemplate = `{ } } }, - "/loginerenew": { + "/loginrenew": { "post": { "security": [ { - "bererToken": [] + "JWT": [] } ], - "description": "renews the users token", - "consumes": [ + "description": "Renews the users token.", + "produces": [ "application/json" ], - "produces": [ - "text/plain" - ], "tags": [ - "User" + "Auth" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "401": { @@ -113,7 +110,12 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "description": "promote chosen user to admin", + "security": [ + { + "JWT": [] + } + ], + "description": "Promote chosen user to site admin", "consumes": [ "application/json" ], @@ -139,7 +141,7 @@ const docTemplate = `{ "200": { "description": "Successfully promoted user", "schema": { - "type": "json" + "$ref": "#/definitions/types.Token" } }, "400": { @@ -173,7 +175,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "User" + "Auth" ], "summary": "Register", "parameters": [ @@ -211,6 +213,11 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { + "security": [ + { + "JWT": [] + } + ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -252,12 +259,14 @@ const docTemplate = `{ }, "/users/all": { "get": { - "description": "lists all users", - "consumes": [ - "application/json" + "security": [ + { + "JWT": [] + } ], + "description": "lists all users", "produces": [ - "text/plain" + "application/json" ], "tags": [ "User" @@ -265,9 +274,12 @@ const docTemplate = `{ "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully signed token for user", + "description": "Successfully returned all users", "schema": { - "type": "json" + "type": "array", + "items": { + "type": "string" + } } }, "401": { @@ -291,16 +303,27 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string" + "type": "string", + "example": "password123" }, "username": { + "type": "string", + "example": "username123" + } + } + }, + "types.Token": { + "type": "object", + "properties": { + "token": { "type": "string" } } } }, "securityDefinitions": { - "bererToken": { + "JWT": { + "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", "type": "apiKey", "name": "Authorization", "in": "header" From 77f028fd39e220f0b16950fc736135d6aaa456ec Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 19:23:35 +0100 Subject: [PATCH 018/114] Prettierignore for generated files --- frontend/.prettierignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 frontend/.prettierignore diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..c49d006 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +goTypes.ts +GenApi.ts \ No newline at end of file From 8d5329146d682a8a4cad817d587f3c2a74c53781 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 19:36:06 +0100 Subject: [PATCH 019/114] New tygo generated goTypes --- frontend/src/Types/goTypes.ts | 52 ++++++++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts index c519aac..7a15741 100644 --- a/frontend/src/Types/goTypes.ts +++ b/frontend/src/Types/goTypes.ts @@ -124,6 +124,44 @@ export interface WeeklyReport { */ signedBy?: number /* int */; } +export interface UpdateWeeklyReport { + /** + * The name of the project, as it appears in the database + */ + projectName: string; + /** + * The name of the user + */ + userName: string; + /** + * The week number + */ + week: number /* int */; + /** + * Total time spent on development + */ + developmentTime: number /* int */; + /** + * Total time spent in meetings + */ + meetingTime: number /* int */; + /** + * Total time spent on administrative tasks + */ + adminTime: number /* int */; + /** + * Total time spent on personal projects + */ + ownWorkTime: number /* int */; + /** + * Total time spent on studying + */ + studyTime: number /* int */; + /** + * Total time spent on testing + */ + testingTime: number /* int */; +} ////////// // source: project.go @@ -151,16 +189,9 @@ export interface NewProject { */ export interface RoleChange { username: string; - role: "project_manager" | "user"; + role: 'project_manager' | 'user'; projectname: string; } - -export interface NewProjMember { - username: string; - projectname: string; - role: string; -} - export interface NameChange { id: number /* int */; name: string; @@ -191,11 +222,6 @@ export interface PublicUser { userId: string; username: string; } - -export interface UserProjectMember { - Username: string; - UserRole: string; -} /** * wrapper type for token */ From d2b4bf2a892db4d1c2d63205ef58036409dd41b0 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 19:36:47 +0100 Subject: [PATCH 020/114] Brand new typescript API interface generated from swagger --- frontend/src/API/GenApi.ts | 358 +++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 frontend/src/API/GenApi.ts diff --git a/frontend/src/API/GenApi.ts b/frontend/src/API/GenApi.ts new file mode 100644 index 0000000..8ca851b --- /dev/null +++ b/frontend/src/API/GenApi.ts @@ -0,0 +1,358 @@ +/* eslint-disable */ +/* tslint:disable */ +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export interface TypesNewUser { + /** @example "password123" */ + password?: string; + /** @example "username123" */ + username?: string; +} + +export interface TypesToken { + token?: string; +} + +export type QueryParamsType = Record; +export type ResponseFormat = keyof Omit; + +export interface FullRequestParams extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseFormat; + /** request body */ + body?: unknown; + /** base url */ + baseUrl?: string; + /** request cancellation token */ + cancelToken?: CancelToken; +} + +export type RequestParams = Omit; + +export interface ApiConfig { + baseUrl?: string; + baseApiParams?: Omit; + securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; + customFetch?: typeof fetch; +} + +export interface HttpResponse extends Response { + data: D; + error: E; +} + +type CancelToken = Symbol | string | number; + +export enum ContentType { + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", +} + +export class HttpClient { + public baseUrl: string = "//localhost:8080/api"; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private abortControllers = new Map(); + private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: {}, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor(apiConfig: ApiConfig = {}) { + Object.assign(this, apiConfig); + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + protected encodeQueryParam(key: string, value: any) { + const encodedKey = encodeURIComponent(key); + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + } + + protected addQueryParam(query: QueryParamsType, key: string) { + return this.encodeQueryParam(key, query[key]); + } + + protected addArrayQueryParam(query: QueryParamsType, key: string) { + const value = query[key]; + return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + } + + protected toQueryString(rawQuery?: QueryParamsType): string { + const query = rawQuery || {}; + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + return keys + .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) + .join("&"); + } + + protected addQueryParams(rawQuery?: QueryParamsType): string { + const queryString = this.toQueryString(rawQuery); + return queryString ? `?${queryString}` : ""; + } + + private contentFormatters: Record any> = { + [ContentType.Json]: (input: any) => + input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, + [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), + [ContentType.FormData]: (input: any) => + Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + formData.append( + key, + property instanceof Blob + ? property + : typeof property === "object" && property !== null + ? JSON.stringify(property) + : `${property}`, + ); + return formData; + }, new FormData()), + [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), + }; + + protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params1, + ...(params2 || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } + + protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { + if (this.abortControllers.has(cancelToken)) { + const abortController = this.abortControllers.get(cancelToken); + if (abortController) { + return abortController.signal; + } + return void 0; + } + + const abortController = new AbortController(); + this.abortControllers.set(cancelToken, abortController); + return abortController.signal; + }; + + public abortRequest = (cancelToken: CancelToken) => { + const abortController = this.abortControllers.get(cancelToken); + + if (abortController) { + abortController.abort(); + this.abortControllers.delete(cancelToken); + } + }; + + public request = async ({ + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const queryString = query && this.toQueryString(query); + const payloadFormatter = this.contentFormatters[type || ContentType.Json]; + const responseFormat = format || requestParams.format; + + return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + }, + signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, + body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), + }).then(async (response) => { + const r = response as HttpResponse; + r.data = null as unknown as T; + r.error = null as unknown as E; + + const data = !responseFormat + ? r + : await response[responseFormat]() + .then((data) => { + if (r.ok) { + r.data = data; + } else { + r.error = data; + } + return r; + }) + .catch((e) => { + r.error = e; + return r; + }); + + if (cancelToken) { + this.abortControllers.delete(cancelToken); + } + + if (!response.ok) throw data; + return data; + }); + }; +} + +/** + * @title TTime API + * @version 0.0.1 + * @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html) + * @baseUrl //localhost:8080/api + * @externalDocs https://swagger.io/resources/open-api/ + * @contact + * + * This is the API for TTime, a time tracking application. + */ +export class GenApi extends HttpClient { + login = { + /** + * @description Logs in a user and returns a JWT token + * + * @tags Auth + * @name LoginCreate + * @summary Login + * @request POST:/login + */ + loginCreate: (body: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/login`, + method: "POST", + body: body, + type: ContentType.Json, + format: "json", + ...params, + }), + }; + loginrenew = { + /** + * @description Renews the users token. + * + * @tags Auth + * @name LoginrenewCreate + * @summary LoginRenews + * @request POST:/loginrenew + * @secure + */ + loginrenewCreate: (params: RequestParams = {}) => + this.request({ + path: `/loginrenew`, + method: "POST", + secure: true, + format: "json", + ...params, + }), + }; + promoteToAdmin = { + /** + * @description Promote chosen user to site admin + * + * @tags User + * @name PromoteToAdminCreate + * @summary PromoteToAdmin + * @request POST:/promoteToAdmin + * @secure + */ + promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/promoteToAdmin`, + method: "POST", + body: NewUser, + secure: true, + type: ContentType.Json, + ...params, + }), + }; + register = { + /** + * @description Register a new user + * + * @tags Auth + * @name RegisterCreate + * @summary Register + * @request POST:/register + */ + registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/register`, + method: "POST", + body: NewUser, + type: ContentType.Json, + ...params, + }), + }; + userdelete = { + /** + * @description UserDelete deletes a user from the database + * + * @tags User + * @name UserdeleteDelete + * @summary UserDelete + * @request DELETE:/userdelete/{username} + * @secure + */ + userdeleteDelete: (username: string, params: RequestParams = {}) => + this.request({ + path: `/userdelete/${username}`, + method: "DELETE", + secure: true, + type: ContentType.Json, + ...params, + }), + }; + users = { + /** + * @description lists all users + * + * @tags User + * @name GetUsers + * @summary ListsAllUsers + * @request GET:/users/all + * @secure + */ + getUsers: (params: RequestParams = {}) => + this.request({ + path: `/users/all`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + }; +} From bc9b01d85a943a49f62b794031a576aa3ee28340 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 19:37:21 +0100 Subject: [PATCH 021/114] Example component GenApiDemo on how to use the new API --- frontend/src/Containers/GenApiDemo.tsx | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 frontend/src/Containers/GenApiDemo.tsx diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx new file mode 100644 index 0000000..27092d8 --- /dev/null +++ b/frontend/src/Containers/GenApiDemo.tsx @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { GenApi } from "../API/GenApi"; + +// Instantiation of the API +const api = new GenApi(); + +export function GenApiDemo(): JSX.Element { + // Sync wrapper around the loginCreate method + const register = async (): Promise => { + const response = await api.login.loginCreate({ + username: "admin", + password: "admin", + }); + console.log(response.data); // This should be the inner type of the response + }; + + // Call the wrapper + useEffect(() => { + void register(); + }); + + return <>; +} From 8b6462abee3e7e615578d7f24e37187f14a82c44 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Fri, 29 Mar 2024 20:19:22 +0100 Subject: [PATCH 022/114] Changed so that username is required to get projects, so that you can get another user's projects (for admin stuff) --- .../handlers/projects/GetUserProject.go | 11 +++++----- backend/main.go | 2 +- .../src/Components/DisplayUserProjects.tsx | 20 +++++-------------- 3 files changed, 12 insertions(+), 21 deletions(-) diff --git a/backend/internal/handlers/projects/GetUserProject.go b/backend/internal/handlers/projects/GetUserProject.go index 99ed63b..6c80515 100644 --- a/backend/internal/handlers/projects/GetUserProject.go +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -4,15 +4,16 @@ import ( db "ttime/internal/database" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" + "github.com/gofiber/fiber/v2/log" ) // GetUserProjects returns all projects that the user is a member of func 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 := db.GetDb(c).GetProjectsForUser(username) diff --git a/backend/main.go b/backend/main.go index cf58280..e4cffae 100644 --- a/backend/main.go +++ b/backend/main.go @@ -112,7 +112,7 @@ func main() { // All project related routes // projectGroup := api.Group("/project") // Not currently in use - api.Get("/getUserProjects", projects.GetUserProjects) + api.Get("/getUserProjects/:username", projects.GetUserProjects) api.Get("/project/:projectId", projects.GetProject) api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index 92ba84f..3df2936 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { Project } from "../Types/goTypes"; import { useNavigate } from "react-router-dom"; import GetProjects from "./GetProjects"; +import { api } from "../API/API"; /** * Renders a component that displays the projects a user is a part of and links to the projects start-page. @@ -11,16 +12,10 @@ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); const navigate = useNavigate(); - 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); - } - }; + GetProjects({ + setProjectsProp: setProjects, + username: localStorage.getItem("username") ?? "", + }); const handleProjectClick = async (projectName: string): Promise => { const token = localStorage.getItem("accessToken") ?? ""; @@ -37,11 +32,6 @@ function DisplayUserProject(): JSX.Element { } }; - // Call getProjects when the component mounts - useEffect(() => { - void getProjects(); - }, []); - return ( <>

Your Projects

From 5f42fa7818ad4919dbdfcba12c3189b98eb6ac25 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Sun, 31 Mar 2024 20:54:00 +0200 Subject: [PATCH 023/114] Fixed types and imports of types --- frontend/src/API/API.ts | 21 ++++++++----------- frontend/src/Components/AddMember.tsx | 9 ++++++-- frontend/src/Components/AddUserToProject.tsx | 3 +-- frontend/src/Components/GetUsersInProject.tsx | 10 ++++++--- frontend/src/Components/ProjectInfoModal.tsx | 5 ++--- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index e7de646..f0b024b 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,13 +1,13 @@ +import { NewProjMember } from "../Components/AddMember"; +import { ProjectMember } from "../Components/GetUsersInProject"; import { NewWeeklyReport, NewUser, User, Project, NewProject, - UserProjectMember, WeeklyReport, StrNameChange, - NewProjMember, } from "../Types/goTypes"; /** @@ -137,7 +137,7 @@ interface API { getAllUsersProject( projectName: string, token: string, - ): Promise>; + ): Promise>; /** * Changes the username of a user in the database. * @param {StrNameChange} data The object containing the previous and new username. @@ -151,7 +151,7 @@ interface API { addUserToProject( user: NewProjMember, token: string, - ): Promise>; + ): Promise>; removeProject( projectName: string, @@ -165,10 +165,7 @@ interface API { * @param {number} reportId The id of the report to sign * @param {string} token The authentication token */ - signReport( - reportId: number, - token: string, - ): Promise>; + signReport(reportId: number, token: string): Promise>; } /** An instance of the API */ @@ -281,7 +278,7 @@ export const api: API = { async addUserToProject( user: NewProjMember, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/addUserToProject", { method: "PUT", @@ -520,7 +517,7 @@ export const api: API = { async getAllUsersProject( projectName: string, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch(`/api/getUsersProject/${projectName}`, { method: "GET", @@ -536,7 +533,7 @@ export const api: API = { message: "Failed to get users", }); } else { - const data = (await response.json()) as UserProjectMember[]; + const data = (await response.json()) as ProjectMember[]; return Promise.resolve({ success: true, data }); } } catch (e) { @@ -622,5 +619,5 @@ export const api: API = { } catch (e) { return { success: false, message: "Failed to sign report" }; } - } + }, }; diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx index d29be68..194afe8 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,5 +1,10 @@ import { APIResponse, api } from "../API/API"; -import { NewProjMember } from "../Types/goTypes"; + +export interface NewProjMember { + username: string; + role: string; + projectname: string; +} /** * Tries to add a member to a project @@ -21,7 +26,7 @@ function AddMember(props: { memberToAdd: NewProjMember }): boolean { props.memberToAdd, localStorage.getItem("accessToken") ?? "", ) - .then((response: APIResponse) => { + .then((response: APIResponse) => { if (response.success) { alert("Member added"); added = true; diff --git a/frontend/src/Components/AddUserToProject.tsx b/frontend/src/Components/AddUserToProject.tsx index 9f4439b..2f5e6af 100644 --- a/frontend/src/Components/AddUserToProject.tsx +++ b/frontend/src/Components/AddUserToProject.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; -import { NewProjMember } from "../Types/goTypes"; import Button from "./Button"; import GetAllUsers from "./GetAllUsers"; -import AddMember from "./AddMember"; +import AddMember, { NewProjMember } from "./AddMember"; import BackButton from "./BackButton"; /** diff --git a/frontend/src/Components/GetUsersInProject.tsx b/frontend/src/Components/GetUsersInProject.tsx index acdd965..a682d3f 100644 --- a/frontend/src/Components/GetUsersInProject.tsx +++ b/frontend/src/Components/GetUsersInProject.tsx @@ -1,7 +1,11 @@ import { Dispatch, useEffect } from "react"; -import { UserProjectMember } from "../Types/goTypes"; import { api } from "../API/API"; +export interface ProjectMember { + Username: string; + UserRole: string; +} + /** * Gets all projects that user is a member of * @param props - A setStateAction for the array you want to put projects in @@ -12,9 +16,9 @@ import { api } from "../API/API"; */ function GetUsersInProject(props: { projectName: string; - setUsersProp: Dispatch>; + setUsersProp: Dispatch>; }): void { - const setUsers: Dispatch> = + const setUsers: Dispatch> = props.setUsersProp; useEffect(() => { const fetchUsers = async (): Promise => { diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index 3075b19..27d4e6e 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -1,7 +1,6 @@ import { useState } from "react"; import Button from "./Button"; -import { UserProjectMember } from "../Types/goTypes"; -import GetUsersInProject from "./GetUsersInProject"; +import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; import { Link } from "react-router-dom"; function ProjectInfoModal(props: { @@ -10,7 +9,7 @@ function ProjectInfoModal(props: { onClose: () => void; onClick: (username: string) => void; }): JSX.Element { - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); if (!props.isVisible) return <>; From 0c8a394f7416e0f35ee275cd2b5a952ca6de0722 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Sun, 31 Mar 2024 20:56:08 +0200 Subject: [PATCH 024/114] Small fix so that it uses component for getting users in a proj --- frontend/src/Components/ProjectMembers.tsx | 30 +++++----------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 60ffcd9..52e8559 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,31 +1,15 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Link, useParams } from "react-router-dom"; -import { api } from "../API/API"; -import { UserProjectMember } from "../Types/goTypes"; +import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); - const [projectMembers, setProjectMembers] = useState([]); + const [projectMembers, setProjectMembers] = useState([]); - useEffect(() => { - const getProjectMembers = async (): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getAllUsersProject(projectName ?? "", token); - console.log(response); - if (response.success) { - setProjectMembers(response.data ?? []); - } else { - console.error(response.message); - } - }; - - void getProjectMembers(); - }, [projectName]); - - interface ProjectMember { - Username: string; - UserRole: string; - } + GetUsersInProject({ + projectName: projectName ?? "", + setUsersProp: setProjectMembers, + }); return ( <> From e7911574be07bc1ffd0be7271480220c096ea926 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Sun, 31 Mar 2024 20:56:40 +0200 Subject: [PATCH 025/114] Removed unused files --- .../AdminPages/AdminProjectManageMembers.tsx | 23 ------------- .../src/Pages/AdminPages/AdminProjectPage.tsx | 33 ------------------- .../AdminPages/AdminProjectViewMemberInfo.tsx | 23 ------------- .../Pages/AdminPages/AdminViewUserInfo.tsx | 28 ---------------- 4 files changed, 107 deletions(-) delete mode 100644 frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx delete mode 100644 frontend/src/Pages/AdminPages/AdminProjectPage.tsx delete mode 100644 frontend/src/Pages/AdminPages/AdminProjectViewMemberInfo.tsx delete mode 100644 frontend/src/Pages/AdminPages/AdminViewUserInfo.tsx diff --git a/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx b/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx deleted file mode 100644 index 7d06a46..0000000 --- a/frontend/src/Pages/AdminPages/AdminProjectManageMembers.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import BackButton from "../../Components/BackButton"; -import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; - -function AdminProjectManageMembers(): JSX.Element { - const content = <>; - - const buttons = ( - <> -
+ ); +} From 9b0a2317010be4a5f96dd3093dc907a0b52bca19 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:14:44 +0200 Subject: [PATCH 031/114] Some fixes to ChangeUsername --- frontend/src/Components/ChangeUsername.tsx | 71 ++++++---------------- 1 file changed, 18 insertions(+), 53 deletions(-) diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index e297a04..78d7da9 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,61 +1,26 @@ -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 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, - ); +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"); + return; + } + api + .changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "") + .then((response: APIResponse) => { if (response.success) { - // Optionally, add a success message or redirect the user - console.log("Username changed successfully"); + alert("Name changed successfully"); + location.reload(); } else { - // Handle the error message - console.error("Failed to change username:", response.message); - setErrorMessage(response.message ?? "Failed to change username"); + alert("Name not changed"); + console.error(response.message); } - } 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); + }) + .catch((error) => { + alert("Name not changed"); + console.error("An error occurred during change:", error); }); - }; - - return ( -
- - {errorMessage &&
{errorMessage}
} - -
- ); } export default ChangeUsername; From 3981190c7a61ef5c1b925e896e5ba80aca27d675 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:16:06 +0200 Subject: [PATCH 032/114] Can now change username in this modal + moved some stuff to a separate modal --- frontend/src/Components/UserInfoModal.tsx | 73 ++++++++++++++++------- 1 file changed, 52 insertions(+), 21 deletions(-) diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index 9d8dc11..7888c6b 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -1,34 +1,38 @@ -import { Link } from "react-router-dom"; import Button from "./Button"; import DeleteUser from "./DeleteUser"; import UserProjectListAdmin from "./UserProjectListAdmin"; +import { useState } from "react"; +import InputField from "./InputField"; +import ChangeUsername from "./ChangeUsername"; +import { StrNameChange } from "../Types/goTypes"; function UserInfoModal(props: { isVisible: boolean; - manageMember: boolean; username: string; onClose: () => void; - onDelete: (username: string) => void; }): JSX.Element { - if (!props.isVisible) return <>; - const ManageUserOrMember = (check: boolean): JSX.Element => { - if (check) { - return ( - -

- (Change Role) -

- - ); + const [showInput, setShowInput] = useState(false); + const [newUsername, setNewUsername] = useState(""); + if (!props.isVisible) { + return <>; + } + + const handleChangeNameView = (): void => { + if (showInput) { + setShowInput(false); + } else { + setShowInput(true); } - return ( - -

- (Change Username) -

- - ); }; + + const handleClickChangeName = (): void => { + const nameChange: StrNameChange = { + prevName: props.username, + newName: newUsername, + }; + ChangeUsername({ nameChange: nameChange }); + }; + return (

{props.username}

- {ManageUserOrMember(props.manageMember)} +

+ (Change Username) +

+ {showInput && ( +
+ +
+ )}

Member of these projects: @@ -62,6 +91,8 @@ function UserInfoModal(props: {

+
+
+ ); +} + +export default MemberInfoModal; From 58deef400a9d448059bad1dfb75fea9fdd9b4d1e Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:17:02 +0200 Subject: [PATCH 034/114] Removed unused code --- frontend/src/Components/ProjectListAdmin.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index f25ee47..7305ea4 100644 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { NewProject } from "../Types/goTypes"; import ProjectInfoModal from "./ProjectInfoModal"; -import UserInfoModal from "./UserInfoModal"; +import MemberInfoModal from "./MemberInfoModal"; /** * A list of projects for admin manage projects page, that sets an onClick @@ -51,13 +51,8 @@ export function ProjectListAdmin(props: { isVisible={projectModalVisible} projectname={projectname} /> - { - return; - }} isVisible={userModalVisible} username={username} /> From dc98fb510e896259169b658b0453c60ceb9627f6 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:17:57 +0200 Subject: [PATCH 035/114] Clears username+password fields on successful register --- frontend/src/Components/Register.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 6192637..8a22806 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -22,6 +22,8 @@ export default function Register(): JSX.Element { const response = await api.registerUser(newUser); if (response.success) { alert("User added!"); + setPassword(""); + setUsername(""); } else { alert("User not added"); setErrMessage(response.message ?? "Unknown error"); From 1212b3c5efb301e895edee48e08a5063afdcf06c Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:20:48 +0200 Subject: [PATCH 036/114] Removed some stuff --- frontend/src/Components/UserListAdmin.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index c08b05c..76cae9f 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import UserInfoModal from "./UserInfoModal"; -import DeleteUser from "./DeleteUser"; /** * A list of users for admin manage users page, that sets an onClick @@ -30,9 +29,7 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element { return ( <> DeleteUser} isVisible={modalVisible} username={username} /> From f3466854c7e0664d4e4045f6617c8382589c2089 Mon Sep 17 00:00:00 2001 From: Peter KW Date: Mon, 1 Apr 2024 02:24:26 +0200 Subject: [PATCH 037/114] Removed unused pages and paths to them in main --- .../Pages/AdminPages/AdminChangeUsername.tsx | 28 ------------------- .../AdminPages/AdminProjectChangeUserRole.tsx | 23 --------------- frontend/src/main.tsx | 11 -------- 3 files changed, 62 deletions(-) delete mode 100644 frontend/src/Pages/AdminPages/AdminChangeUsername.tsx delete mode 100644 frontend/src/Pages/AdminPages/AdminProjectChangeUserRole.tsx diff --git a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx deleted file mode 100644 index b130fae..0000000 --- a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import BackButton from "../../Components/BackButton"; -import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; -import ChangeUsername from "../../Components/ChangeUsername"; - -function AdminChangeUsername(): JSX.Element { - const content = ( - <> - - - ); - - const buttons = ( - <> -