diff --git a/.gitignore b/.gitignore index 3b1c6d3..c50fe24 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,11 @@ diagram.puml backend/*.png backend/*.jpg backend/*.svg +__pycache__ /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..15a550c 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 @@ -46,7 +47,7 @@ itest: make build ./bin/$(PROC_NAME) >/dev/null 2>&1 & sleep 1 # Adjust if needed - python ../testing.py + python ../testing/testing.py || pkill $(PROC_NAME) pkill $(PROC_NAME) # Get dependencies target @@ -104,6 +105,17 @@ default: build 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 \ + --output ../frontend/src/API \ + --name GenApi.ts \ + +./docs/swagger.json: + swag init -outputTypes json + .PHONY: docfmt docfmt: swag fmt diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 322c812..c8b020d 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": { @@ -111,9 +108,64 @@ const docTemplate = `{ } } }, + "/promote/{projectName}": { + "put": { + "security": [ + { + "JWT": [] + } + ], + "description": "Promote a user to project manager", + "consumes": [ + "text/plain" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "Auth" + ], + "summary": "Promote to project manager", + "parameters": [ + { + "type": "string", + "description": "Project name", + "name": "projectName", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "User name", + "name": "userName", + "in": "query", + "required": true + } + ], + "responses": { + "403": { + "description": "Forbidden", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, "/promoteToAdmin": { "post": { - "description": "promote chosen user to admin", + "security": [ + { + "JWT": [] + } + ], + "description": "Promote chosen user to site admin", "consumes": [ "application/json" ], @@ -137,13 +189,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully prometed user", + "description": "Successfully promoted user", "schema": { - "type": "json" + "$ref": "#/definitions/types.Token" } }, "400": { - "description": "bad request", + "description": "Bad request", "schema": { "type": "string" } @@ -173,7 +225,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "User" + "Auth" ], "summary": "Register", "parameters": [ @@ -211,6 +263,11 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { + "security": [ + { + "JWT": [] + } + ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -252,12 +309,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 +324,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 +353,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" diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index f4c0f6e..22e11e9 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -17,6 +17,7 @@ type Database interface { AddUser(username string, password string) error CheckUser(username string, password string) bool RemoveUser(username string) error + RemoveUserFromProject(username string, projectname string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error @@ -35,7 +36,7 @@ type Database interface { GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) - GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error) + GetAllWeeklyReports(username string, projectname string) ([]types.WeeklyReportList, error) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) @@ -43,6 +44,7 @@ type Database interface { GetProjectTimes(projectName string) (map[string]int, error) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error RemoveProject(projectname string) error + GetUserName(id int) (string, error) } // This struct is a wrapper type that holds the database connection @@ -86,6 +88,10 @@ const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles JOIN projects ON user_roles.project_id = projects.id WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'` +const removeUserFromProjectQuery = `DELETE FROM user_roles + WHERE user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?)` + // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database @@ -147,6 +153,11 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) return err } +func (d *Db) RemoveUserFromProject(username string, projectname string) error { + _, err := d.Exec(removeUserFromProjectQuery, username, projectname) + return err +} + // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { // Execute the SQL query to change the user's role @@ -339,9 +350,14 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { return err } + managerQuery := `SELECT project_id FROM user_roles + WHERE user_id = ? + AND project_id = (SELECT project_id FROM weekly_reports WHERE report_id = ?) + AND p_role = 'project_manager'` + // Retrieve the project ID associated with the project manager var managerProjectID int - err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId) + err = d.Get(&managerProjectID, managerQuery, projectManagerId, reportId) if err != nil { return err } @@ -463,8 +479,8 @@ func (d *Db) Migrate() error { return nil } -// GetWeeklyReportsUser retrieves weekly reports for a specific user and project. -func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) { +// GetAllWeeklyReports retrieves weekly reports for a specific user and project. +func (d *Db) GetAllWeeklyReports(username string, projectName string) ([]types.WeeklyReportList, error) { query := ` SELECT wr.week, @@ -601,3 +617,9 @@ func (d *Db) RemoveProject(projectname string) error { _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) return err } + +func (d *Db) GetUserName(id int) (string, error) { + var username string + err := d.Get(&username, "SELECT username FROM users WHERE id = ?", id) + return username, err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index fe3e6cd..a691a4d 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -705,7 +705,7 @@ func TestGetWeeklyReportsUser(t *testing.T) { t.Error("AddWeeklyReport failed:", err) } - reports, err := db.GetWeeklyReportsUser("testuser", "testproject") + reports, err := db.GetAllWeeklyReports("testuser", "testproject") if err != nil { t.Error("GetWeeklyReportsUser failed:", err) } @@ -962,6 +962,5 @@ func TestRemoveProject(t *testing.T) { if len(projects) != 0 { t.Error("RemoveProject failed: expected 0, got", len(projects)) } - + } - \ No newline at end of file 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/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index ab74f1a..70499b0 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -21,6 +21,12 @@ VALUES ("projecttest3","test project3", 1); INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) VALUES (1,1,"project_manager"); +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (1,2,"project_manager"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (1,3,"project_manager"); + INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) VALUES (2,1,"member"); 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_project_related.go b/backend/internal/handlers/handlers_project_related.go deleted file mode 100644 index d63d7eb..0000000 --- a/backend/internal/handlers/handlers_project_related.go +++ /dev/null @@ -1,315 +0,0 @@ -package handlers - -import ( - "strconv" - "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 (gs *GState) 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 := gs.Db.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 { - - projectID := c.Params("projectID") - username := c.Params("username") - - if err := gs.Db.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 (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) - - // Then dip into the database to get the projects - projects, err := gs.Db.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 (gs *GState) 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 := gs.Db.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 := gs.Db.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 (gs *GState) 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 := gs.Db.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 (gs *GState) 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 := gs.Db.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 := gs.Db.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 := gs.Db.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 (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 { - 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 := gs.Db.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 = gs.Db.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 (gs *GState) 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 := gs.Db.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 (gs *GState) 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 := gs.Db.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 := gs.Db.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 := gs.Db.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 (gs *GState) 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) - 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 := gs.Db.RemoveProject(projectName); err != nil { - return c.Status(500).SendString((err.Error())) - } - - return c.Status(200).SendString("Project deleted") -} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go deleted file mode 100644 index 9fa83cc..0000000 --- a/backend/internal/handlers/handlers_report_related.go +++ /dev/null @@ -1,218 +0,0 @@ -package handlers - -import ( - "strconv" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -func (gs *GState) 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 := gs.Db.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 (gs *GState) 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 := gs.Db.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 (gs *GState) 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 := gs.Db.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 = gs.Db.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 (gs *GState) 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 := gs.Db.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") - } - if !isProjectManager { - log.Info("User is not a project manager") - return c.Status(401).SendString("User is not a project manager") - } - - log.Info("User is Project Manager: ", isProjectManager) - - // Call the database function to get the unsigned weekly reports - reports, err := gs.Db.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 (gs *GState) 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 := gs.Db.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 (gs *GState) 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 := gs.Db.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/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/projects/AddUserToProject.go b/backend/internal/handlers/projects/AddUserToProject.go new file mode 100644 index 0000000..3195314 --- /dev/null +++ b/backend/internal/handlers/projects/AddUserToProject.go @@ -0,0 +1,42 @@ +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 { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + username := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Add the user to the project with the specified role + err = db.GetDb(c).AddUserToProject(username, project, "member") + if err != nil { + log.Info("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User : ", username, " added to project: ", project) + 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..6c80515 --- /dev/null +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -0,0 +1,26 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// GetUserProjects returns all projects that the user is a member of +func GetUserProjects(c *fiber.Ctx) error { + 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) + 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..6c5d455 --- /dev/null +++ b/backend/internal/handlers/projects/ProjectRoleChange.go @@ -0,0 +1,51 @@ +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()) + } + + // Check if user is trying to change its own role + if username == data.UserName { + log.Info("Can't change your own role") + return c.Status(403).SendString("Can't change your own role") + } + + log.Info("Changing role for user: ", data.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(data.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/PromoteToPm.go b/backend/internal/handlers/projects/PromoteToPm.go new file mode 100644 index 0000000..c587b65 --- /dev/null +++ b/backend/internal/handlers/projects/PromoteToPm.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" +) + +// @Summary Promote to project manager +// @Description Promote a user to project manager +// @Tags Auth +// @Security JWT +// @Accept plain +// @Produce plain +// @Param projectName path string true "Project name" +// @Param userName query string true "User name" +// @Failure 500 {string} string "Internal server error" +// @Failure 403 {string} string "Forbidden" +// @Router /promote/{projectName} [put] +// +// Login logs in a user and returns a JWT token +// Promote to project manager +func PromoteToPm(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + new_pm_name := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Add the user to the project with the specified role + err = db.GetDb(c).ChangeUserRole(new_pm_name, project, "project_manager") + if err != nil { + log.Info("Error promoting user to project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User : ", new_pm_name, " promoted to project manager in project: ", project) + 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/RemoveUserFromProject.go b/backend/internal/handlers/projects/RemoveUserFromProject.go new file mode 100644 index 0000000..7aefcf8 --- /dev/null +++ b/backend/internal/handlers/projects/RemoveUserFromProject.go @@ -0,0 +1,40 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func RemoveUserFromProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + pm_name := claims["name"].(string) + + project := c.Params("projectName") + username := c.Query("userName") + + // Check if the user is a project manager + isPM, err := db.GetDb(c).IsProjectManager(pm_name, project) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isPM { + log.Info("User: ", pm_name, " is not a project manager in project: ", project) + return c.Status(403).SendString("User is not a project manager") + } + + // Remove the user from the project + if err = db.GetDb(c).RemoveUserFromProject(username, project); err != nil { + log.Info("Error removing user from project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User : ", username, " removed from project: ", project) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/reports/GetAllWeeklyReports.go b/backend/internal/handlers/reports/GetAllWeeklyReports.go new file mode 100644 index 0000000..d9778b8 --- /dev/null +++ b/backend/internal/handlers/reports/GetAllWeeklyReports.go @@ -0,0 +1,56 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// GetAllWeeklyReports retrieves all weekly reports for a user in a specific project +func GetAllWeeklyReports(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 project name and week from query parameters + projectName := c.Params("projectName") + target_user := c.Query("targetUser") // The user whose reports are being requested + + // If the target user is not empty, use it as the username + if target_user == "" { + target_user = username + } + + log.Info(username, " trying to get all weekly reports for: ", target_user) + + if projectName == "" { + log.Info("Missing project name") + return c.Status(400).SendString("Missing project name") + } + + // If the user is not a project manager, they can only view their own reports + pm, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !(pm || target_user == username) { + log.Info("Unauthorized access") + return c.Status(403).SendString("Unauthorized access") + } + + // Retrieve weekly reports for the user in the project from the database + reports, err := db.GetDb(c).GetAllWeeklyReports(target_user, projectName) + if err != nil { + log.Error("Error getting weekly reports for user:", target_user, "in project:", projectName, ":", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning weekly report") + // Return the retrieved weekly report + return c.JSON(reports) +} 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..2b6827e --- /dev/null +++ b/backend/internal/handlers/reports/GetWeeklyReport.go @@ -0,0 +1,65 @@ +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) + + // Extract project name and week from query parameters + projectName := c.Query("projectName") + week := c.Query("week") + target_user := c.Query("targetUser") // The user whose report is being requested + + // If the target user is not empty, use it as the username + if target_user == "" { + target_user = username + } + + log.Info(username, " trying to get weekly report for: ", target_user) + + 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") + } + + // If the token user is not an admin, check if the target user is the same as the token user + pm, err := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking if user is project manager:", err) + return c.Status(500).SendString(err.Error()) + } + + if !(pm || target_user == username) { + log.Info("Unauthorized access") + return c.Status(403).SendString("Unauthorized access") + } + + // Call the database function to get the weekly report + report, err := db.GetDb(c).GetWeeklyReport(target_user, 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/SignReport.go b/backend/internal/handlers/reports/SignReport.go new file mode 100644 index 0000000..a486ecc --- /dev/null +++ b/backend/internal/handlers/reports/SignReport.go @@ -0,0 +1,41 @@ +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" +) + +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) + + // 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") + } + + // Get the project manager's ID + projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) + if err != nil { + log.Info("Failed to get project manager ID for user: ", projectManagerUsername) + return c.Status(500).SendString("Failed to get project manager ID") + } + + // Call the database function to sign the weekly report + 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/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/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/GetUserName.go b/backend/internal/handlers/users/GetUserName.go new file mode 100644 index 0000000..82b6cc8 --- /dev/null +++ b/backend/internal/handlers/users/GetUserName.go @@ -0,0 +1,32 @@ +package users + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" +) + +// Return the username of a user given their user id +func GetUserName(c *fiber.Ctx) error { + // Check the query params for userId + user_id_string := c.Query("userId") + if user_id_string == "" { + return c.Status(400).SendString("Missing user id") + } + + // Convert to int + user_id, err := strconv.Atoi(user_id_string) + if err != nil { + return c.Status(400).SendString("Invalid user id") + } + + // Get the username from the database + username, err := db.GetDb(c).GetUserName(user_id) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Send the nuclear launch codes to north korea + return c.JSON(fiber.Map{"username": username}) +} diff --git a/backend/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..5ac5df0 --- /dev/null +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -0,0 +1,32 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// @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() + 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..42c52a5 --- /dev/null +++ b/backend/internal/handlers/users/Login.go @@ -0,0 +1,66 @@ +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" +) + +// @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 + + 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..3926ce4 --- /dev/null +++ b/backend/internal/handlers/users/LoginRenew.go @@ -0,0 +1,50 @@ +package users + +import ( + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// @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() + + // 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"], + }) + + // 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) // 500 + } + + 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..3f0a6d3 --- /dev/null +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -0,0 +1,45 @@ +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 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 + 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..b9e0c78 --- /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" +) + +// @Summary Register +// @Description Register a new user +// @Tags Auth +// @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] +// +// 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 { + 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..491a1b3 --- /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" +) + +// @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") + + // 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/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) diff --git a/backend/main.go b/backend/main.go index 669bbc7..7b19dd9 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" @@ -23,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. @@ -54,24 +57,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 +86,54 @@ 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.Get("/users/all", users.ListAllUsers) + api.Get("/project/getAllUsers", users.GetAllUsersProject) + api.Get("/username", users.GetUserName) + api.Post("/login", users.Login) + api.Post("/register", users.Register) + api.Post("/loginrenew", users.LoginRenew) + api.Post("/promoteToAdmin", users.PromoteToAdmin) + 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("/getProjectTimes/:projectName", projects.GetProjectTimesHandler) + api.Get("/getUserProjects/:username", projects.GetUserProjects) + api.Get("/project/:projectId", projects.GetProject) + api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) + api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) + api.Post("/project", projects.CreateProject) + api.Post("/ProjectRoleChange", projects.ProjectRoleChange) + api.Put("/promoteToPm/:projectName", projects.PromoteToPm) + api.Put("/addUserToProject/:projectName", projects.AddUserToProjectHandler) + api.Delete("/removeUserFromProject/:projectName", projects.RemoveUserFromProject) + api.Delete("/removeProject/:projectName", projects.RemoveProject) + api.Delete("/project/:projectID", projects.DeleteProject) + + // All report related routes + // reportGroup := api.Group("/report") // Not currently in use + api.Get("/getWeeklyReport", reports.GetWeeklyReport) + api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) + api.Get("/getAllWeeklyReports/:projectName", reports.GetAllWeeklyReports) + api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) + api.Put("/signReport/:reportId", reports.SignReport) + api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) 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 diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 0160e15..86ad6dc 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,13 +1,16 @@ +import { AddMemberInfo } from "../Components/AddMember"; +import { ProjectRoleChange } from "../Components/ChangeRole"; +import { projectTimes } from "../Components/GetProjectTimes"; +import { ProjectMember } from "../Components/GetUsersInProject"; import { + UpdateWeeklyReport, NewWeeklyReport, NewUser, User, Project, NewProject, - UserProjectMember, WeeklyReport, StrNameChange, - NewProjMember, } from "../Types/goTypes"; /** @@ -73,10 +76,7 @@ interface API { * @param {string} token The authentication token. * @returns {Promise>} A promise resolving to an API response with the created project. */ - createProject( - project: NewProject, - token: string, - ): Promise>; + createProject(project: NewProject, token: string): Promise>; /** Submits a weekly report * @param {NewWeeklyReport} weeklyReport The weekly report object. @@ -88,16 +88,31 @@ interface API { token: string, ): Promise>; - /** Gets a weekly report for a specific user, project and week + /** + * Updates a weekly report. + * @param {UpdateWeeklyReport} weeklyReport The updated weekly report object. + * @param {string} token The authentication token. + * @returns {Promise>} A promise containing the API response with the updated report. + */ + updateWeeklyReport( + weeklyReport: UpdateWeeklyReport, + token: string, + ): Promise>; + + /** Gets a weekly report for a specific user, project and week. + * Keep in mind that the user within the token needs to be PM + * of the project to get the report, unless the user is the target user. * @param {string} projectName The name of the project. * @param {string} week The week number. * @param {string} token The authentication token. + * @param {string} targetUser The username of the target user. Defaults to token user. * @returns {Promise>} A promise resolving to an API response with the retrieved report. */ getWeeklyReport( projectName: string, week: string, token: string, + targetUser?: string, ): Promise>; /** @@ -107,16 +122,21 @@ interface API { * @param {string} token The token of the user * @returns {APIResponse} A list of weekly reports */ - getWeeklyReportsForUser( + getAllWeeklyReportsForUser( projectName: string, token: string, + targetUser?: string, ): 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. @@ -124,6 +144,16 @@ interface API { */ getProject(id: number): Promise>; + /** Gets a projects reported time + * @param {string} projectName The name of the project. + * @param {string} token The usertoken. + * @returns {Promise>} A promise resolving to an API response containing the project times. + */ + getProjectTimes( + projectName: string, + token: string, + ): Promise>; + /** Gets a list of all users. * @param {string} token The authentication token of the requesting user. * @returns {Promise>} A promise resolving to an API response containing the list of users. @@ -133,7 +163,18 @@ interface API { getAllUsersProject( projectName: string, token: string, - ): Promise>; + ): Promise>; + + /** Gets all unsigned reports in a project. + * @param {string} projectName The name of the project. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response containing the list of unsigned reports. + */ + getUnsignedReportsInProject( + projectName: string, + token: string, + ): Promise>; + /** * Changes the username of a user in the database. * @param {StrNameChange} data The object containing the previous and new username. @@ -144,15 +185,60 @@ interface API { data: StrNameChange, token: string, ): Promise>; - addUserToProject( - user: NewProjMember, + /** + * Changes the role of a user in the database. + * @param {RoleChange} roleInfo The object containing the previous and new username. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response. + */ + changeUserRole( + roleInfo: ProjectRoleChange, token: string, - ): Promise>; + ): Promise>; + + addUserToProject( + addMemberInfo: AddMemberInfo, + token: string, + ): Promise>; + + removeUserFromProject( + user: string, + project: string, + token: string, + ): Promise>; removeProject( 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>; + + /** + * Promotes a user to project manager within a project. + * + * @param {string} userName The username of the user to promote + * @param {string} projectName The name of the project to promote the user in + * @returns {Promise} A promise resolving to an API response. + */ + promoteToPm( + userName: string, + projectName: string, + token: string, + ): Promise>; + /** + * Get the username from the id + * @param {number} id The id of the user + * @param {string} token Your token + */ + getUsername(id: number, token: string): Promise>; } /** An instance of the API */ @@ -240,7 +326,7 @@ export const api: API = { async createProject( project: NewProject, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/project", { method: "POST", @@ -254,27 +340,28 @@ export const api: API = { if (!response.ok) { return { success: false, message: "Failed to create project" }; } else { - const data = (await response.json()) as Project; - return { success: true, data }; + return { success: true }; } } catch (e) { - return { success: false, message: "Failed to create project" }; + return { success: false, message: "Failed to create project!" }; } }, async addUserToProject( - user: NewProjMember, + addMemberInfo: AddMemberInfo, token: string, - ): Promise> { + ): Promise> { try { - const response = await fetch("/api/addUserToProject", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify(user), - }); + ); if (!response.ok) { return { success: false, message: "Failed to add member" }; @@ -286,6 +373,31 @@ export const api: API = { } }, + async removeUserFromProject( + user: string, + project: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/removeUserFromProject/${project}?userName=${user}`, + { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + if (!response.ok) { + return { success: false, message: "Failed to remove member" }; + } + } catch (e) { + return { success: false, message: "Failed to remove member" }; + } + return { success: true, message: "Removed member" }; + }, + async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -307,9 +419,39 @@ export const api: API = { } }, - async getUserProjects(token: string): Promise> { + async changeUserRole( + roleInfo: ProjectRoleChange, + token: string, + ): Promise> { try { - const response = await fetch("/api/getUserProjects", { + const response = await fetch("/api/ProjectRoleChange", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(roleInfo), + }); + + if (!response.ok) { + if (response.status === 403) { + return { success: false, message: "Cannot change your own role" }; + } + return { success: false, message: "Could not change role" }; + } else { + return { success: true }; + } + } catch (e) { + return { success: false, message: "Could not change role" }; + } + }, + + async getUserProjects( + username: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getUserProjects/${username}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -334,6 +476,37 @@ export const api: API = { } }, + async getProjectTimes( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getProjectTimes/${projectName}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: + "Fetch error: " + response.status + ", failed to get project times", + }); + } else { + const data = (await response.json()) as projectTimes; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "API error! Could not get times.", + }); + } + }, + async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, @@ -365,14 +538,46 @@ export const api: API = { } }, + async updateWeeklyReport( + weeklyReport: UpdateWeeklyReport, + token: string, + ): Promise> { + try { + const response = await fetch("/api/updateWeeklyReport", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(weeklyReport), + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to update weekly report", + }; + } + + const data = await response.text(); + return { success: true, message: data }; + } catch (e) { + return { + success: false, + message: "Failed to update weekly report", + }; + } + }, + async getWeeklyReport( projectName: string, week: string, token: string, + targetUser?: string, ): Promise> { try { const response = await fetch( - `/api/getWeeklyReport?projectName=${projectName}&week=${week}`, + `/api/getWeeklyReport?projectName=${projectName}&week=${week}&targetUser=${targetUser ?? ""}`, { method: "GET", headers: { @@ -393,18 +598,22 @@ export const api: API = { } }, - async getWeeklyReportsForUser( + async getAllWeeklyReportsForUser( projectName: string, token: string, + targetUser?: string, ): Promise> { try { - const response = await fetch(`/api/getWeeklyReportsUser/${projectName}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/getAllWeeklyReports/${projectName}?targetUser=${targetUser ?? ""}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - }); + ); if (!response.ok) { return { @@ -501,7 +710,7 @@ export const api: API = { async getAllUsersProject( projectName: string, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch(`/api/getUsersProject/${projectName}`, { method: "GET", @@ -517,7 +726,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) { @@ -528,6 +737,38 @@ export const api: API = { } }, + async getUnsignedReportsInProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/getUnsignedReports/${projectName}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { + success: false, + message: + "Failed to get unsigned reports for project: Response code " + + response.status, + }; + } else { + const data = (await response.json()) as WeeklyReport[]; + return { success: true, data }; + } + } catch (e) { + return { + success: false, + message: "Failed to get unsigned reports for project, unknown error", + }; + } + }, + async changeUserName( data: StrNameChange, token: string, @@ -557,7 +798,7 @@ export const api: API = { token: string, ): Promise> { try { - const response = await fetch(`/api/projectdelete/${projectName}`, { + const response = await fetch(`/api/removeProject/${projectName}`, { method: "DELETE", headers: { "Content-Type": "application/json", @@ -581,4 +822,79 @@ 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" }; + } + }, + + async promoteToPm( + userName: string, + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/promoteToPm/${projectName}?userName=${userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + if (!response.ok) { + return { + success: false, + message: "Failed to promote user to project manager", + }; + } + } catch (e) { + return { + success: false, + message: "Failed to promote user to project manager", + }; + } + return { success: true, message: "User promoted to project manager" }; + }, + + async getUsername(id: number, token: string): Promise> { + try { + const response = await fetch(`/api/username?userId=${id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { success: false, message: "Failed to get username" }; + } else { + const data = (await response.json()) as string; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to get username" }; + } + }, }; diff --git a/frontend/src/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, + }), + }; +} diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx index d29be68..d8036b7 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,39 +1,35 @@ -import { APIResponse, api } from "../API/API"; -import { NewProjMember } from "../Types/goTypes"; +import { api } from "../API/API"; + +export interface AddMemberInfo { + userName: string; + projectName: string; +} /** * Tries to add a member to a project - * @param {Object} props - A NewProjMember - * @returns {boolean} True if added, false if not + * @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name + * @returns {Promise} */ -function AddMember(props: { memberToAdd: NewProjMember }): boolean { - let added = false; - if ( - props.memberToAdd.username === "" || - props.memberToAdd.role === "" || - props.memberToAdd.projectname === "" - ) { - alert("All fields must be filled before adding"); - return added; +async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise { + if (props.memberToAdd.userName === "") { + alert("You must choose at least one user to add"); + return; } - api - .addUserToProject( + try { + const response = await api.addUserToProject( props.memberToAdd, localStorage.getItem("accessToken") ?? "", - ) - .then((response: APIResponse) => { - if (response.success) { - alert("Member added"); - added = true; - } else { - alert("Member not added"); - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during member add:", error); - }); - return added; + ); + if (response.success) { + alert(`[${props.memberToAdd.userName}] added`); + } else { + alert(`[${props.memberToAdd.userName}] not added`); + console.error(response.message); + } + } catch (error) { + alert(`[${props.memberToAdd.userName}] not added`); + console.error("An error occurred during member add:", error); + } } export default AddMember; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index f5f4a08..c8a1c66 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,38 +1,10 @@ import { useState } from "react"; -import { APIResponse, api } from "../API/API"; -import { NewProject, Project } from "../Types/goTypes"; +import { api } from "../API/API"; +import { NewProject } from "../Types/goTypes"; import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -/** - * Tries to add a project to the system - * @param {Object} props - Project name and description - * @returns {boolean} True if created, false if not - */ -function CreateProject(props: { name: string; description: string }): boolean { - const project: NewProject = { - name: props.name, - description: props.description, - }; - - let created = false; - - api - .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - created = true; - } else { - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during creation:", error); - }); - return created; -} - /** * Provides UI for adding a project to the system. * @returns {JSX.Element} - Returns the component UI for adding a project @@ -41,6 +13,33 @@ function AddProject(): JSX.Element { const [name, setName] = useState(""); const [description, setDescription] = useState(""); + /** + * Tries to add a project to the system + */ + const handleCreateProject = async (): Promise => { + const project: NewProject = { + name: name.replace(/ /g, ""), + description: description.trim(), + }; + try { + const response = await api.createProject( + project, + localStorage.getItem("accessToken") ?? "", + ); + if (response.success) { + alert(`${project.name} added!`); + setDescription(""); + setName(""); + } else { + alert("Project not added, name could be taken"); + console.error(response.message); + } + } catch (error) { + alert("Project not added"); + console.error(error); + } + }; + return (
@@ -48,7 +47,7 @@ function AddProject(): JSX.Element { className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit" onSubmit={(e) => { e.preventDefault(); - CreateProject({ name: name, description: description }); + void handleCreateProject(); }} > Create a new project - { - setName(e.target.value); - }} - /> - { - setDescription(e.target.value); - }} - /> +
+ { + e.preventDefault(); + setName(e.target.value); + }} + /> + { + e.preventDefault(); + setDescription(e.target.value); + }} + /> +
diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx index 4fa9ad8..0d5916b 100644 --- a/frontend/src/Components/AllTimeReportsInProject.tsx +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -17,7 +17,7 @@ function AllTimeReportsInProject(): JSX.Element { useEffect(() => { const getWeeklyReports = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getWeeklyReportsForUser( + const response = await api.getAllWeeklyReportsForUser( projectName ?? "", token, ); diff --git a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx index 09ca6dc..4218f0a 100644 --- a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx +++ b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx @@ -1,8 +1,9 @@ //Info: This component is used to display all the time reports for a project. It will display the week number, //total time spent, and if the report has been signed or not. The user can click on a report to edit it. import { useEffect, useState } from "react"; -import { NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport } from "../Types/goTypes"; import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; /** * Renders a component that displays all the time reports for a specific project. @@ -11,15 +12,15 @@ import { Link, useParams } from "react-router-dom"; function AllTimeReportsInProject(): JSX.Element { const { username } = useParams(); const { projectName } = useParams(); - const [weeklyReports, setWeeklyReports] = useState([]); + const [weeklyReports, setWeeklyReports] = useState([]); - /* // Call getProjects when the component mounts useEffect(() => { const getWeeklyReports = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getWeeklyReportsForUser( + const response = await api.getAllWeeklyReportsForUser( projectName ?? "", token, + username ?? "", ); console.log(response); if (response.success) { @@ -27,41 +28,10 @@ function AllTimeReportsInProject(): JSX.Element { } else { console.error(response.message); } - }; */ - // Mock data - const getWeeklyReports = async (): Promise => { - // Simulate a delay - await Promise.resolve(); - const mockWeeklyReports: NewWeeklyReport[] = [ - { - projectName: "Project 1", - week: 1, - developmentTime: 10, - meetingTime: 2, - adminTime: 1, - ownWorkTime: 3, - studyTime: 4, - testingTime: 5, - }, - { - projectName: "Project 1", - week: 2, - developmentTime: 8, - meetingTime: 2, - adminTime: 1, - ownWorkTime: 3, - studyTime: 4, - testingTime: 5, - }, - // Add more reports as needed - ]; + }; - // Use the mock data instead of the real data - setWeeklyReports(mockWeeklyReports); - }; - useEffect(() => { void getWeeklyReports(); - }, []); + }, [projectName, username]); return ( <> @@ -90,7 +60,7 @@ function AllTimeReportsInProject(): JSX.Element {

{"Signed: "} - NO + {newWeeklyReport.signedBy ? "YES" : "NO"}

diff --git a/frontend/src/Components/ChangeRole.tsx b/frontend/src/Components/ChangeRole.tsx new file mode 100644 index 0000000..54b1468 --- /dev/null +++ b/frontend/src/Components/ChangeRole.tsx @@ -0,0 +1,37 @@ +import { APIResponse, api } from "../API/API"; + +export interface ProjectRoleChange { + username: string; + role: "project_manager" | "member" | ""; + projectname: string; +} + +export default function ChangeRole(roleChangeInfo: ProjectRoleChange): void { + if ( + roleChangeInfo.username === "" || + roleChangeInfo.role === "" || + roleChangeInfo.projectname === "" + ) { + // FOR DEBUG + // console.log(roleChangeInfo.role + ": Role"); + // console.log(roleChangeInfo.projectname + ": P-Name"); + // console.log(roleChangeInfo.username + ": U-name"); + alert("You have to select a role"); + return; + } + api + .changeUserRole(roleChangeInfo, localStorage.getItem("accessToken") ?? "") + .then((response: APIResponse) => { + if (response.success) { + alert("Role changed successfully"); + location.reload(); + } else { + alert(response.message); + console.error(response.message); + } + }) + .catch((error) => { + alert(error); + console.error("An error occurred during change:", error); + }); +} diff --git a/frontend/src/Components/ChangeRoleView.tsx b/frontend/src/Components/ChangeRoleView.tsx new file mode 100644 index 0000000..782ad8d --- /dev/null +++ b/frontend/src/Components/ChangeRoleView.tsx @@ -0,0 +1,73 @@ +import { useState } from "react"; +import Button from "./Button"; +import ChangeRole, { ProjectRoleChange } from "./ChangeRole"; + +export default function ChangeRoleView(props: { + projectName: string; + username: string; +}): JSX.Element { + const [selectedRole, setSelectedRole] = useState< + "project_manager" | "member" | "" + >(""); + + const handleRoleChange = ( + event: React.ChangeEvent, + ): void => { + if (event.target.value === "member") { + setSelectedRole(event.target.value); + } else if (event.target.value === "project_manager") { + setSelectedRole(event.target.value); + } + }; + + const handleSubmit = (event: React.FormEvent): void => { + event.preventDefault(); + const roleChangeInfo: ProjectRoleChange = { + username: props.username, + projectname: props.projectName, + role: selectedRole, + }; + ChangeRole(roleChangeInfo); + }; + + return ( +
+

Select role:

+
+
+
+ +
+
+ +
+
+
+ ); +} diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index e297a04..2f73bb6 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,61 +1,29 @@ -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 === "" || + props.nameChange.newName === props.nameChange.prevName + ) { + alert("You have to give a new name\n\nName not changed"); + 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, name could be taken"); + 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; diff --git a/frontend/src/Components/DeleteProject.tsx b/frontend/src/Components/DeleteProject.tsx new file mode 100644 index 0000000..4add857 --- /dev/null +++ b/frontend/src/Components/DeleteProject.tsx @@ -0,0 +1,33 @@ +import { api, APIResponse } from "../API/API"; + +/** + * Use to delete a project from the system + * @param {string} props.projectToDelete - The projectname of project to delete + * @returns {void} Nothing + * @example + * const exampleProjectName = "project"; + * DeleteProject({ projectToDelete: exampleProjectName }); + */ + +function DeleteProject(props: { projectToDelete: string }): void { + api + .removeProject( + props.projectToDelete, + localStorage.getItem("accessToken") ?? "", + ) + .then((response: APIResponse) => { + if (response.success) { + alert("Project has been deleted!"); + location.reload(); + } else { + alert("Project has not been deleted"); + console.error(response.message); + } + }) + .catch((error) => { + alert("project has not been deleted"); + console.error("An error occurred during deletion:", error); + }); +} + +export default DeleteProject; diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx index d1dbc7f..7c5e8e8 100644 --- a/frontend/src/Components/DeleteUser.tsx +++ b/frontend/src/Components/DeleteUser.tsx @@ -3,7 +3,7 @@ import { api, APIResponse } from "../API/API"; /** * Use to remove a user from the system - * @param props - The username of user to remove + * @param {string} props.usernameToDelete - The username of user to remove * @returns {boolean} True if removed, false if not * @example * const exampleUsername = "user"; @@ -29,7 +29,7 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { }) .catch((error) => { alert("User has not been deleted"); - console.error("An error occurred during creation:", error); + console.error("An error occurred during deletion:", error); }); return removed; } diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx index 780f20c..25a1da3 100644 --- a/frontend/src/Components/DisplayUnsignedReports.tsx +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -1,93 +1,38 @@ import { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; +import { WeeklyReport } from "../Types/goTypes"; -interface UnsignedReports { - projectName: string; - username: string; - week: number; - signed: boolean; -} - -/** - * Renders a component that displays the projects a user is a part of and links to the projects start-page. - * @returns The JSX element representing the component. - */ function DisplayUserProject(): JSX.Element { const { projectName } = useParams(); - const [unsignedReports, setUnsignedReports] = useState([]); - //const navigate = useNavigate(); + const [unsignedReports, setUnsignedReports] = useState([]); + const [usernames, setUsernames] = useState([]); + const token = localStorage.getItem("accessToken") ?? ""; - // const getUnsignedReports = async (): Promise => { - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.getUserProjects(token); - // console.log(response); - // if (response.success) { - // setUnsignedReports(response.data ?? []); - // } else { - // console.error(response.message); - // } - // }; - - // const handleReportClick = async (projectName: string): Promise => { - // const username = localStorage.getItem("username") ?? ""; - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.checkIfProjectManager( - // username, - // projectName, - // token, - // ); - // if (response.success) { - // if (response.data) { - // navigate(`/PMProjectPage/${projectName}`); - // } else { - // navigate(`/project/${projectName}`); - // } - // } else { - // // handle error - // console.error(response.message); - // } - // }; - - const getUnsignedReports = async (): Promise => { - // Simulate a delay - await Promise.resolve(); - - // Use mock data - const reports: UnsignedReports[] = [ - { - projectName: "projecttest", - username: "user1", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user2", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user3", - week: 2, - signed: false, - }, - { - projectName: "projecttest", - username: "user4", - week: 2, - signed: false, - }, - ]; - - // Set the state with the mock data - setUnsignedReports(reports); - }; - - // Call getProjects when the component mounts useEffect(() => { + const getUnsignedReports = async (): Promise => { + const response = await api.getUnsignedReportsInProject( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setUnsignedReports(response.data ?? []); + const usernamesPromises = (response.data ?? []).map((report) => + api.getUsername(report.userId, token), + ); + const usernamesResponses = await Promise.all(usernamesPromises); + const usernames = usernamesResponses.map( + (res) => (res.data as { username?: string }).username ?? "", + ); + setUsernames(usernames); + } else { + console.error(response.message); + } + }; + void getUnsignedReports(); - }, []); + }, [projectName, token]); return ( <> @@ -95,32 +40,40 @@ function DisplayUserProject(): JSX.Element { All Unsigned Reports In: {projectName}{" "}
- {unsignedReports.map( - (unsignedReport: UnsignedReports, index: number) => ( -

-
-
-

{unsignedReport.username}

- Week: -

{unsignedReport.week}

- Signed: -

NO

-
-
-
- -

- View Report -

- -
+ {unsignedReports.map((unsignedReport: WeeklyReport, index: number) => ( +

+
+
+ Username: +

{usernames[index]}

{" "} + Week: +

{unsignedReport.week}

+ Total Time: +

+ {unsignedReport.developmentTime + + unsignedReport.meetingTime + + unsignedReport.adminTime + + unsignedReport.ownWorkTime + + unsignedReport.studyTime + + unsignedReport.testingTime} +

+ Signed: +

NO

+
+
+
+ +

+ View Report +

+
-

- ), - )} +
+

+ ))}
); diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index 29e4bcb..65bd4f5 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from "react"; +import { useState } from "react"; import { Project } from "../Types/goTypes"; import { useNavigate } from "react-router-dom"; +import GetProjects from "./GetProjects"; import { api } from "../API/API"; /** @@ -11,22 +12,20 @@ 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") ?? ""; const response = await api.checkIfProjectManager(projectName, token); + console.log(response.data); if (response.success) { - if (response.data) { + if ( + (response.data as unknown as { isProjectManager: boolean }) + .isProjectManager + ) { navigate(`/PMProjectPage/${projectName}`); } else { navigate(`/project/${projectName}`); @@ -37,11 +36,6 @@ function DisplayUserProject(): JSX.Element { } }; - // Call getProjects when the component mounts - useEffect(() => { - void getProjects(); - }, []); - return ( <>

Your Projects

diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 384359e..d56ee42 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport, UpdateWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -22,6 +22,7 @@ export default function GetWeeklyReport(): JSX.Element { projectName: string; fetchedWeek: string; }>(); + const username = localStorage.getItem("userName") ?? ""; console.log(projectName, fetchedWeek); useEffect(() => { @@ -60,8 +61,9 @@ export default function GetWeeklyReport(): JSX.Element { void fetchWeeklyReport(); }, [projectName, fetchedWeek, token]); - const handleNewWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { + const handleUpdateWeeklyReport = async (): Promise => { + const updateWeeklyReport: UpdateWeeklyReport = { + userName: username, projectName: projectName ?? "", week, developmentTime, @@ -72,7 +74,7 @@ export default function GetWeeklyReport(): JSX.Element { testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + await api.updateWeeklyReport(updateWeeklyReport, token); }; const navigate = useNavigate(); @@ -89,7 +91,8 @@ export default function GetWeeklyReport(): JSX.Element { return; } e.preventDefault(); - void handleNewWeeklyReport(); + void handleUpdateWeeklyReport(); + alert("Changes submitted"); navigate(-1); }} > @@ -128,7 +131,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -152,7 +160,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -176,7 +189,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -200,7 +218,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -224,7 +247,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -248,7 +276,12 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> diff --git a/frontend/src/Components/GetProjectTimes.tsx b/frontend/src/Components/GetProjectTimes.tsx new file mode 100644 index 0000000..38288ec --- /dev/null +++ b/frontend/src/Components/GetProjectTimes.tsx @@ -0,0 +1,59 @@ +import { Dispatch, SetStateAction, useEffect } from "react"; +import { api } from "../API/API"; + +/** + * Interface for reported time per category + total time reported + */ +export interface projectTimes { + admin: number; + development: number; + meeting: number; + own_work: number; + study: number; + testing: number; + totalTime?: number; +} + +/** + * Gets all reported times for this project + * @param {Dispatch} props.setTimesProp - A setStateAction for the map you want to put times in + * @param {string} props.projectName - Username + * @returns {void} Nothing + * @example + * const projectName = "Example"; + * const [times, setTimes] = useState(); + * GetProjectTimes({ setTimesProp: setTimes, projectName: projectName }); + */ +function GetProjectTimes(props: { + setTimesProp: Dispatch>; + projectName: string; +}): void { + const setTimes: Dispatch> = + props.setTimesProp; + useEffect(() => { + const fetchUsers = async (): Promise => { + try { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getProjectTimes(props.projectName, token); + if (response.success && response.data) { + // Calculates total time reported + response.data.totalTime = response.data.admin; + response.data.totalTime += response.data.development; + response.data.totalTime += response.data.meeting; + response.data.totalTime += response.data.own_work; + response.data.totalTime += response.data.study; + response.data.totalTime += response.data.testing; + setTimes(response.data); + } else { + console.error("Failed to fetch project times:", response.message); + } + } catch (error) { + console.error("Error fetching times:", error); + } + }; + + void fetchUsers(); + }, [props.projectName, setTimes]); +} + +export default GetProjectTimes; diff --git a/frontend/src/Components/GetProjects.tsx b/frontend/src/Components/GetProjects.tsx index d6ab1f7..bd6c303 100644 --- a/frontend/src/Components/GetProjects.tsx +++ b/frontend/src/Components/GetProjects.tsx @@ -4,14 +4,17 @@ import { api } from "../API/API"; /** * Gets all projects that user is a member of - * @param props - A setStateAction for the array you want to put projects in + * @param {Dispatch} props.setProjectsProp - A setStateAction for the array you want to put projects in + * @param {string} props.username - Username * @returns {void} Nothing * @example + * const username = "Example"; * const [projects, setProjects] = useState([]); - * GetAllUsers({ setProjectsProp: setProjects }); + * GetProjects({ setProjectsProp: setProjects, username: username }); */ function GetProjects(props: { setProjectsProp: Dispatch>; + username: string; }): void { const setProjects: Dispatch> = props.setProjectsProp; @@ -19,7 +22,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 +34,7 @@ function GetProjects(props: { }; void fetchUsers(); - }, [setProjects]); + }, [props.username, setProjects]); } export default GetProjects; diff --git a/frontend/src/Components/GetUsersInProject.tsx b/frontend/src/Components/GetUsersInProject.tsx index acdd965..eb32e9b 100644 --- a/frontend/src/Components/GetUsersInProject.tsx +++ b/frontend/src/Components/GetUsersInProject.tsx @@ -1,20 +1,25 @@ 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 + * Gets all members of a project + * @param string - The project's name + * @param Dispatch - A setStateAction for the array you want to put members in * @returns {void} Nothing * @example - * const [projects, setProjects] = useState([]); - * GetAllUsers({ setProjectsProp: setProjects }); + * const [users, setUsers] = useState([]); + * GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); */ function GetUsersInProject(props: { projectName: string; - setUsersProp: Dispatch>; + setUsersProp: Dispatch>; }): void { - const setUsers: Dispatch> = + const setUsers: Dispatch> = props.setUsersProp; useEffect(() => { const fetchUsers = async (): Promise => { @@ -24,10 +29,10 @@ function GetUsersInProject(props: { if (response.success) { setUsers(response.data ?? []); } else { - console.error("Failed to fetch projects:", response.message); + console.error("Failed to fetch members:", response.message); } } catch (error) { - console.error("Error fetching projects:", error); + console.error("Error fetching members:", error); } }; void fetchUsers(); diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index eb4fa5a..9be2f4b 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,6 +1,6 @@ //info: Header component to display the header of the page including the logo and user information where thr user can logout import { useState } from "react"; -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import backgroundImage from "../assets/1.jpg"; /** @@ -9,23 +9,33 @@ import backgroundImage from "../assets/1.jpg"; */ function Header(): JSX.Element { const [isOpen, setIsOpen] = useState(false); + const username = localStorage.getItem("username"); + const navigate = useNavigate(); const handleLogout = (): void => { localStorage.clear(); }; + const handleNavigation = (): void => { + if (username === "admin") { + navigate("/admin"); + } else { + navigate("/yourProjects"); + } + }; + return (
- +
TTIME Logo - +
) => void; }): JSX.Element { return ( -
+
+
+
+
+ ); +} + +export default MemberInfoModal; diff --git a/frontend/src/Components/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index f684b0c..ffc8b21 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -62,7 +62,7 @@ export default function NewWeeklyReport(): JSX.Element { const success = await handleNewWeeklyReport(); if (!success) { alert( - "A Time Report for this week already exists, please go to the edit page to edit it or change week number.", + "Error occurred! Your connection to the server might be lost or a time report for this week already exists, please check your connection or go to the edit page to edit your report or change week number.", ); return; } @@ -139,7 +139,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -163,7 +168,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -187,7 +197,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -211,7 +226,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -235,7 +255,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -259,7 +284,12 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> diff --git a/frontend/src/Components/OtherUsersTR.tsx b/frontend/src/Components/OtherUsersTR.tsx index 2b00e16..ce7761c 100644 --- a/frontend/src/Components/OtherUsersTR.tsx +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -29,6 +29,7 @@ export default function OtherUsersTR(): JSX.Element { projectName ?? "", fetchedWeek?.toString() ?? "0", token, + username ?? "", ); if (response.success) { @@ -86,6 +87,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={developmentTime === 0 ? "" : developmentTime} + readOnly /> @@ -97,6 +99,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={meetingTime === 0 ? "" : meetingTime} + readOnly /> @@ -108,6 +111,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={adminTime === 0 ? "" : adminTime} + readOnly /> @@ -119,6 +123,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={ownWorkTime === 0 ? "" : ownWorkTime} + readOnly /> @@ -130,6 +135,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={studyTime === 0 ? "" : studyTime} + readOnly /> @@ -141,6 +147,7 @@ export default function OtherUsersTR(): JSX.Element { min="0" className="border-2 border-black rounded-md text-center w-1/2" value={testingTime === 0 ? "" : testingTime} + readOnly /> diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index 3075b19..1f98d79 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -1,31 +1,54 @@ -import { useState } from "react"; +import { useEffect, useRef, 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"; +import GetProjectTimes, { projectTimes } from "./GetProjectTimes"; +import DeleteProject from "./DeleteProject"; function ProjectInfoModal(props: { - isVisible: boolean; projectname: string; onClose: () => void; onClick: (username: string) => void; }): JSX.Element { - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); + const [times, setTimes] = useState(); + const totalTime = useRef(0); GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); - if (!props.isVisible) return <>; + + GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname }); + + useEffect(() => { + if (times?.totalTime !== undefined) { + totalTime.current = times.totalTime; + } + }, [times]); return (
-
-
+
+

- {localStorage.getItem("projectName") ?? ""} + {props.projectname}

-

Project members:

-
+
+

Statistics:

+
+
+

Number of members: {users.length}

+

+ Total time reported:{" "} + {Math.floor(totalTime.current / 60 / 24) + " d "} + {Math.floor((totalTime.current / 60) % 24) + " h "} + {(totalTime.current % 60) + " m "} +

+
+
+

Project members:

+
+
    {users.map((user) => ( @@ -45,31 +68,44 @@ function ProjectInfoModal(props: { ))}
-
-
-
diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index f25ee47..294a131 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 @@ -18,7 +18,7 @@ export function ProjectListAdmin(props: { projects: NewProject[]; }): JSX.Element { const [projectModalVisible, setProjectModalVisible] = useState(false); - const [projectname, setProjectname] = useState(""); + const [projectName, setProjectName] = useState(""); const [userModalVisible, setUserModalVisible] = useState(false); const [username, setUsername] = useState(""); @@ -28,39 +28,36 @@ export function ProjectListAdmin(props: { }; const handleClickProject = (projectname: string): void => { - setProjectname(projectname); - localStorage.setItem("projectName", projectname); + setProjectName(projectname); setProjectModalVisible(true); }; const handleCloseProject = (): void => { - setProjectname(""); + setProjectName(""); setProjectModalVisible(false); }; const handleCloseUser = (): void => { - setProjectname(""); + setUsername(""); setUserModalVisible(false); }; return ( <> - - { - return; - }} - isVisible={userModalVisible} - username={username} - /> + {projectModalVisible && ( + + )} + {userModalVisible && ( + + )}
    {props.projects.map((project) => ( 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 ( <> diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 6192637..be35a74 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -15,15 +15,21 @@ export default function Register(): JSX.Element { const [errMessage, setErrMessage] = useState(); const handleRegister = async (): Promise => { + if (username === "" || password === "") { + alert("Must provide username and password"); + return; + } const newUser: NewUser = { - username: username ?? "", + username: username?.replace(/ /g, "") ?? "", password: password ?? "", }; const response = await api.registerUser(newUser); if (response.success) { - alert("User added!"); + alert(`${newUser.username} added!`); + setPassword(""); + setUsername(""); } else { - alert("User not added"); + alert("User not added, name could be taken"); setErrMessage(response.message ?? "Unknown error"); console.error(errMessage); } @@ -47,22 +53,24 @@ export default function Register(): JSX.Element {

    Register New User

    - { - setUsername(e.target.value); - }} - /> - { - setPassword(e.target.value); - }} - /> +
    + { + setUsername(e.target.value); + }} + /> + { + setPassword(e.target.value); + }} + /> +
    + )} +

    Member of these projects:

    + +
    +
-
-
); 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} /> diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index 1b7b923..bc85c5b 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -1,35 +1,17 @@ -import { useEffect, useState } from "react"; -import { api } from "../API/API"; +import { useState } from "react"; import { Project } from "../Types/goTypes"; +import GetProjects from "./GetProjects"; -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 response = await api.getUserProjects(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(); - }, []); + GetProjects({ setProjectsProp: setProjects, username: props.username }); return ( -
-
    +
    +
      {projects.map((project) => ( -
    • +
    • {project.name}
    • ))} diff --git a/frontend/src/Components/ViewOtherTimeReport.tsx b/frontend/src/Components/ViewOtherTimeReport.tsx index 32e0716..3689854 100644 --- a/frontend/src/Components/ViewOtherTimeReport.tsx +++ b/frontend/src/Components/ViewOtherTimeReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -18,6 +18,7 @@ export default function GetOtherUsersReport(): JSX.Element { const [ownWorkTime, setOwnWorkTime] = useState(0); const [studyTime, setStudyTime] = useState(0); const [testingTime, setTestingTime] = useState(0); + const [reportId, setReportId] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; const { projectName } = useParams(); @@ -30,8 +31,9 @@ export default function GetOtherUsersReport(): JSX.Element { projectName ?? "", fetchedWeek?.toString() ?? "0", token, + username ?? "", ); - + console.log(response); if (response.success) { const report: WeeklyReport = response.data ?? { reportId: 0, @@ -45,6 +47,7 @@ export default function GetOtherUsersReport(): JSX.Element { studyTime: 0, testingTime: 0, }; + setReportId(report.reportId); setWeek(report.week); setDevelopmentTime(report.developmentTime); setMeetingTime(report.meetingTime); @@ -60,32 +63,33 @@ export default function GetOtherUsersReport(): JSX.Element { void fetchUsersWeeklyReport(); }); - const handleSignWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { - projectName: projectName ?? "", - week, - developmentTime, - meetingTime, - adminTime, - ownWorkTime, - studyTime, - testingTime, - }; - - await api.submitWeeklyReport(newWeeklyReport, token); + const handleSignWeeklyReport = async (): Promise => { + const response = await api.signReport(reportId, token); + if (response.success) { + return true; + } else { + return false; + } }; const navigate = useNavigate(); return ( <> -

      {username}'s Report

      +

      {username}'s Report

      { e.preventDefault(); - void handleSignWeeklyReport(); - navigate(-1); + void (async (): Promise => { + const success = await handleSignWeeklyReport(); + if (!success) { + alert("Failed to sign report!"); + return; + } + alert("Report successfully signed!"); + navigate(-1); + })(); }} >
      @@ -112,7 +116,10 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime === 0 ? "" : developmentTime} + defaultValue={ + developmentTime === 0 ? "" : developmentTime + } + readOnly /> @@ -123,7 +130,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime === 0 ? "" : meetingTime} + defaultValue={meetingTime === 0 ? "" : meetingTime} + readOnly /> @@ -134,7 +142,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime === 0 ? "" : adminTime} + defaultValue={adminTime === 0 ? "" : adminTime} + readOnly /> @@ -145,7 +154,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime === 0 ? "" : ownWorkTime} + defaultValue={ownWorkTime === 0 ? "" : ownWorkTime} + readOnly /> @@ -156,7 +166,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime === 0 ? "" : studyTime} + defaultValue={studyTime === 0 ? "" : studyTime} + readOnly /> @@ -167,7 +178,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime === 0 ? "" : testingTime} + defaultValue={testingTime === 0 ? "" : testingTime} + readOnly /> 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 <>; +} 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 = ( - <> -