diff --git a/.gitignore b/.gitignore index bdbfff8..313b735 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ dist/ .vscode/ .idea/ .DS_Store +.go.work.sum # Ignore configuration files .env diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5cbb13f..e2aa366 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -32,6 +32,7 @@ type Database interface { GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error + IsSiteAdmin(username string) (bool, error) } // This struct is a wrapper type that holds the database connection @@ -106,7 +107,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { // GetProject retrieves a specific project by its ID. func (d *Db) GetProject(projectId int) (types.Project, error) { var project types.Project - err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") + err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId) + if err != nil { + println("Error getting project: ", err) + } return project, err } @@ -313,6 +317,26 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { return err } +// IsSiteAdmin checks if a given username is a site admin +func (d *Db) IsSiteAdmin(username string) (bool, error) { + // Define the SQL query to check if the user is a site admin + query := ` + SELECT COUNT(*) FROM site_admin + JOIN users ON site_admin.admin_id = users.id + WHERE users.username = ? + ` + + // Execute the query + var count int + err := d.Get(&count, query, username) + if err != nil { + return false, err + } + + // If count is greater than 0, the user is a site admin + return count > 0, nil +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 09de45b..a7f3878 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -536,3 +536,33 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") } } + +func TestGetProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Retrieve the added project + project, err := db.GetProject(1) + if err != nil { + t.Error("GetProject failed:", err) + } + + // Check if the retrieved project matches the expected values + if project.Name != "testproject" { + t.Errorf("Expected Name to be testproject, got %s", project.Name) + } +} diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index c8beb1c..566d549 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -17,6 +17,9 @@ type GlobalState interface { 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 // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index 6a430e9..3732249 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -66,6 +66,10 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { func (gs *GState) GetProject(c *fiber.Ctx) error { // Extract the project ID from the request parameters or body projectID := c.Params("projectID") + if projectID == "" { + return c.Status(400).SendString("No project ID provided") + } + println("Getting project with ID: ", projectID) // Parse the project ID into an integer projectIDInt, err := strconv.Atoi(projectID) @@ -80,6 +84,7 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { } // Return the project as JSON + println("Returning project: ", project.Name) return c.JSON(project) } @@ -96,3 +101,45 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { // 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 (gs *GState) 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 { + println("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) + println("Admin username from claims:", adminUsername) + + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + println("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + println("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 = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + if err != nil { + println("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + println("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 509bd67..291d068 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -64,30 +64,42 @@ func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { return c.JSON(report) } +type ReportId struct { + ReportId int +} + func (gs *GState) SignReport(c *fiber.Ctx) error { + println("Signing report...") // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) - managerUsername := claims["name"].(string) + projectManagerUsername := claims["name"].(string) - // Extract the report ID and project manager ID from request parameters - reportID, err := strconv.Atoi(c.Params("reportId")) - if err != nil { - return c.Status(400).SendString("Invalid report ID") + // Extract report ID from the request query parameters + // reportID := c.Query("reportId") + rid := new(ReportId) + if err := c.BodyParser(rid); err != nil { + return err } + println("Signing report for: ", rid.ReportId) + // reportIDInt, err := strconv.Atoi(rid.ReportId) + // println("Signing report for: ", rid.ReportId) + // if err != nil { + // return c.Status(400).SendString("Invalid report ID") + // } - // Call the database function to get the project manager ID - managerID, err := gs.Db.GetUserId(managerUsername) + // Get the project manager's ID + projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) if err != nil { return c.Status(500).SendString("Failed to get project manager ID") } + println("blabla", projectManagerID) // Call the database function to sign the weekly report - err = gs.Db.SignWeeklyReport(reportID, managerID) + err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) if err != nil { - return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error()) + return c.Status(500).SendString(err.Error()) } - // Return success response return c.Status(200).SendString("Weekly report signed successfully") } diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 0619ea5..0f7c047 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "time" "ttime/internal/types" @@ -122,3 +123,25 @@ func (gs *GState) ListAllUsers(c *fiber.Ctx) error { // Return the list of users as JSON return c.JSON(users) } + +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 + + println("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 { + fmt.Println("Error promoting user to admin:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + println("User promoted to admin successfully:", username) // Debug print + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/main.go b/backend/main.go index bc33942..3e2fb75 100644 --- a/backend/main.go +++ b/backend/main.go @@ -78,7 +78,11 @@ func main() { server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) + server.Get("/api/project/:projectId", gs.GetProject) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) + server.Post("/api/signReport", gs.SignReport) + server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) + server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 7a1ccd0..6078513 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -29,11 +29,6 @@ interface API { project: NewProject, token: string, ): Promise>; - /** Gets all the projects of a user*/ - getUserProjects( - username: string, - token: string, - ): Promise>; /** Submit a weekly report */ submitWeeklyReport( project: NewWeeklyReport, @@ -46,6 +41,13 @@ interface API { week: string, token: string, ): Promise>; + /** Gets all the projects of a user*/ + getUserProjects( + username: string, + token: string, + ): Promise>; + /** Gets a project from id*/ + getProject(id: number): Promise>; } // Export an instance of the API @@ -148,7 +150,10 @@ export const api: API = { } }, - async getUserProjects(token: string): Promise> { + async getUserProjects( + username: string, + token: string, + ): Promise> { try { const response = await fetch("/api/getUserProjects", { method: "GET", @@ -156,6 +161,7 @@ export const api: API = { "Content-Type": "application/json", Authorization: "Bearer " + token, }, + body: JSON.stringify({ username }), }); if (!response.ok) { @@ -253,4 +259,30 @@ export const api: API = { return Promise.resolve({ success: false, message: "Failed to login" }); } }, + + // Gets a projet by id, currently untested since we have no javascript-based tests + async getProject(id: number): Promise> { + try { + const response = await fetch(`/api/project/${id}`, { + method: "GET", + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to get project: Response code " + response.status, + }; + } else { + const data = (await response.json()) as Project; + return { success: true, data }; + } + // The code below is garbage but satisfies the linter + // This needs fixing, do not copy this pattern + } catch (e: unknown) { + return { + success: false, + message: "Failed to get project: " + (e as Error).toString(), + }; + } + }, }; diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 9321d73..b0e8771 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -50,8 +50,8 @@ export default function GetWeeklyReport(): JSX.Element { } }; - fetchWeeklyReport(); - }, []); + void fetchWeeklyReport(); + }, [projectName, token, username, week]); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index d8b2a47..7b003cb 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -23,6 +23,7 @@ export default function Register(): JSX.Element { nav("/"); // Instantly navigate to the login page } else { setErrMessage(response.message ?? "Unknown error"); + console.error(errMessage); } }; @@ -47,7 +48,7 @@ export default function Register(): JSX.Element { { setUsername(e.target.value); }} @@ -55,7 +56,7 @@ export default function Register(): JSX.Element { { setPassword(e.target.value); }} diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx new file mode 100644 index 0000000..69258a1 --- /dev/null +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -0,0 +1,43 @@ +import React, { useEffect, useState } from "react"; +import { api } from "../API/API"; +import { Project } from "../Types/goTypes"; + +const UserProjectListAdmin: React.FC = () => { + const [projects, setProjects] = useState([]); + + useEffect(() => { + const fetchProjects = async (): Promise => { + try { + const token = localStorage.getItem("accessToken") ?? ""; + const username = "NoUser"; // getUsernameFromContext(); // Assuming you have a function to get the username from your context + + const response = await api.getUserProjects(username, token); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error("Failed to fetch projects:", response.message); + } + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + void fetchProjects(); + }, []); + + return ( +
+

User Projects

+
    + {projects.map((project) => ( +
  • + {project.name} + {/* Add any additional project details you want to display */} +
  • + ))} +
+
+ ); +}; + +export default UserProjectListAdmin; diff --git a/frontend/src/Pages/AdminPages/AdminAddUser.tsx b/frontend/src/Pages/AdminPages/AdminAddUser.tsx index 38f00d5..4af2eb7 100644 --- a/frontend/src/Pages/AdminPages/AdminAddUser.tsx +++ b/frontend/src/Pages/AdminPages/AdminAddUser.tsx @@ -1,5 +1,5 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; -import Button from "../../Components/Button"; import Register from "../../Components/Register"; function AdminAddUser(): JSX.Element { @@ -11,13 +11,7 @@ function AdminAddUser(): JSX.Element { const buttons = ( <> -