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 f871755..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,12 +36,15 @@ 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) IsProjectManager(username string, projectname string) (bool, error) 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 @@ -84,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 @@ -145,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 @@ -337,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 } @@ -354,6 +372,51 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { return err } +func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) { + // Define the SQL query to fetch unsigned reports for a given user + query := ` + SELECT + report_id, + user_id, + project_id, + week, + development_time, + meeting_time, + admin_time, + own_work_time, + study_time, + testing_time, + signed_by + FROM + weekly_reports + WHERE + signed_by IS NULL + AND project_id = (SELECT id FROM projects WHERE name = ?) + ` + + // Execute the query + rows, err := d.Queryx(query, projectName) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var reports []types.WeeklyReport + for rows.Next() { + var report types.WeeklyReport + if err := rows.StructScan(&report); err != nil { + return nil, err + } + reports = append(reports, report) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return reports, nil +} + // IsSiteAdmin checks if a given username is a site admin func (d *Db) IsSiteAdmin(username string) (bool, error) { // Define the SQL query to check if the user is a site admin @@ -416,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, @@ -549,3 +612,14 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { return totalTime, nil } + +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 b68d446..a691a4d 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -470,6 +470,47 @@ func TestGetWeeklyReport(t *testing.T) { // Check other fields similarly } +func TestGetUnsignedWeeklyReports(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser1", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + reports, err := db.GetUnsignedWeeklyReports("testproject") + if err != nil { + t.Error("GetUnsignedWeeklyReports failed:", err) + } + + if reports == nil { + t.Error("Expected non-nil reports, got nil") + } +} + // TestSignWeeklyReport tests SignWeeklyReport function of the database func TestSignWeeklyReport(t *testing.T) { db, err := setupState() @@ -664,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) } @@ -894,3 +935,32 @@ func TestUpdateWeeklyReport(t *testing.T) { t.Error("UpdateWeeklyReport failed: report not updated correctly") } } + +func TestRemoveProject(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Promote user to Admin + err = db.PromoteToAdmin("demouser") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } + + // Remove project + err = db.RemoveProject("projecttest") + if err != nil { + t.Error("RemoveProject failed:", err) + } + + // Check if the project was removed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if len(projects) != 0 { + t.Error("RemoveProject failed: expected 0, got", len(projects)) + } + +} 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 092fbb0..70499b0 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -7,6 +7,8 @@ VALUES ("user", "123"); INSERT OR IGNORE INTO users(username, password) VALUES ("user2", "123"); +INSERT OR IGNORE INTO site_admin VALUES (1); + INSERT OR IGNORE INTO projects(name,description,owner_user_id) VALUES ("projecttest","test project", 1); @@ -19,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 49c8c09..0000000 --- a/backend/internal/handlers/global_state.go +++ /dev/null @@ -1,42 +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 - UpdateWeeklyReport(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 f64d013..0000000 --- a/backend/internal/handlers/handlers_project_related.go +++ /dev/null @@ -1,289 +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) -} \ No newline at end of file diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go deleted file mode 100644 index 0e72ead..0000000 --- a/backend/internal/handlers/handlers_report_related.go +++ /dev/null @@ -1,177 +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") -} - -// 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 39788ae..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 another user") - return c.Status(403).SendString("You can only 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 1967708..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,35 +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.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) + // 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 c6cef66..86ad6dc 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,11 +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, } from "../Types/goTypes"; /** @@ -47,7 +52,6 @@ interface API { * @returns {Promise>} A promise containing the API response indicating if the user is a project manager. */ checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise>; @@ -72,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. @@ -87,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>; /** @@ -106,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. @@ -123,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. @@ -132,7 +163,82 @@ 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. + * @param {string} token The authentication token. + * @returns {Promise>} A promise resolving to an API response. + */ + changeUserName( + data: StrNameChange, + token: string, + ): Promise>; + /** + * 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>; + + 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 */ @@ -170,19 +276,17 @@ export const api: API = { ): Promise> { try { const response = await fetch(`/api/userdelete/${username}`, { - method: "POST", + method: "DELETE", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(username), }); - if (!response.ok) { - return { success: false, message: "Failed to remove user" }; + return { success: false, message: "Could not remove user" }; } else { - const data = (await response.json()) as User; - return { success: true, data }; + return { success: true }; } } catch (e) { return { success: false, message: "Failed to remove user" }; @@ -190,19 +294,20 @@ export const api: API = { }, async checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise> { try { - const response = await fetch("/api/checkIfProjectManager", { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/checkIfProjectManager/${projectName}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify({ username, projectName }), - }); + ); if (!response.ok) { return { @@ -214,14 +319,14 @@ export const api: API = { return { success: true, data }; } } catch (e) { - return { success: false, message: "fuck" }; + return { success: false, message: "Failed to check if project manager" }; } }, async createProject( project: NewProject, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/project", { method: "POST", @@ -235,14 +340,64 @@ 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( + addMemberInfo: AddMemberInfo, + token: string, + ): Promise> { + try { + const response = await fetch( + `/api/addUserToProject/${addMemberInfo.projectName}/?userName=${addMemberInfo.userName}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }, + ); + + if (!response.ok) { + return { success: false, message: "Failed to add member" }; + } else { + return { success: true, message: "Added member" }; + } + } catch (e) { + return { success: false, message: "Failed to add member" }; + } + }, + + async 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", { @@ -264,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", @@ -291,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, @@ -322,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: { @@ -350,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 { @@ -458,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", @@ -474,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) { @@ -484,4 +736,165 @@ 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, + ): Promise> { + try { + const response = await fetch("/api/changeUserName", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + return { success: false, message: "Failed to change username" }; + } else { + return { success: true }; + } + } catch (e) { + return { success: false, message: "Failed to change username" }; + } + }, + + async removeProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/removeProject/${projectName}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } else { + const data = await response.text(); + return Promise.resolve({ success: true, message: data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } + }, + + 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 new file mode 100644 index 0000000..d8036b7 --- /dev/null +++ b/frontend/src/Components/AddMember.tsx @@ -0,0 +1,35 @@ +import { api } from "../API/API"; + +export interface AddMemberInfo { + userName: string; + projectName: string; +} + +/** + * Tries to add a member to a project + * @param {AddMemberInfo} props.membertoAdd - Contains user's name and project's name + * @returns {Promise} + */ +async function AddMember(props: { memberToAdd: AddMemberInfo }): Promise { + if (props.memberToAdd.userName === "") { + alert("You must choose at least one user to add"); + return; + } + try { + const response = await api.addUserToProject( + props.memberToAdd, + localStorage.getItem("accessToken") ?? "", + ); + 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); + }} + /> +
+

+
+ ); +} + +export default AddUserToProject; 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 new file mode 100644 index 0000000..4218f0a --- /dev/null +++ b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx @@ -0,0 +1,73 @@ +//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 { 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. + * @returns {JSX.Element} representing the component. + */ +function AllTimeReportsInProject(): JSX.Element { + const { username } = useParams(); + const { projectName } = useParams(); + const [weeklyReports, setWeeklyReports] = useState([]); + + useEffect(() => { + const getWeeklyReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getAllWeeklyReportsForUser( + projectName ?? "", + token, + username ?? "", + ); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } + }; + + void getWeeklyReports(); + }, [projectName, username]); + + return ( + <> +

{username}'s Time Reports

+
+ {weeklyReports.map((newWeeklyReport, index) => ( + +
+

+ {"Week: "} + {newWeeklyReport.week} +

+

+ {"Total Time: "} + {newWeeklyReport.developmentTime + + newWeeklyReport.meetingTime + + newWeeklyReport.adminTime + + newWeeklyReport.ownWorkTime + + newWeeklyReport.studyTime + + newWeeklyReport.testingTime}{" "} + min +

+

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

+
+ + ))} +
+ + ); +} + +export default AllTimeReportsInProject; 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 3c35e94..2f73bb6 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,34 +1,29 @@ -import React, { useState } from "react"; -import InputField from "./InputField"; +import { APIResponse, api } from "../API/API"; +import { StrNameChange } from "../Types/goTypes"; -function ChangeUsername(): JSX.Element { - const [newUsername, setNewUsername] = useState(""); - - const handleChange = (e: React.ChangeEvent): void => { - setNewUsername(e.target.value); - }; - - // const handleSubmit = async (): Promise => { - // try { - // // Call the API function to update the username - // await api.updateUsername(newUsername); - // // Optionally, add a success message or redirect the user - // } catch (error) { - // console.error("Error updating username:", error); - // // Optionally, handle the error - // } - // }; - - return ( -
- -
- ); +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) { + alert("Name changed successfully"); + location.reload(); + } else { + alert("Name not changed, name could be taken"); + console.error(response.message); + } + }) + .catch((error) => { + alert("Name not changed"); + console.error("An error occurred during change:", error); + }); } 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 db49724..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"; @@ -11,7 +11,6 @@ import { api, APIResponse } from "../API/API"; */ function DeleteUser(props: { usernameToDelete: string }): boolean { - //console.log(props.usernameToDelete); FOR DEBUG let removed = false; api .removeUser( @@ -20,13 +19,17 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { ) .then((response: APIResponse) => { if (response.success) { + alert("User has been deleted!"); + location.reload(); removed = true; } else { + alert("User has not been deleted"); console.error(response.message); } }) .catch((error) => { - console.error("An error occurred during creation:", error); + alert("User has not been deleted"); + console.error("An error occurred during deletion:", error); }); return removed; } diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx new file mode 100644 index 0000000..25a1da3 --- /dev/null +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -0,0 +1,82 @@ +import { useState, useEffect } from "react"; +import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; +import { WeeklyReport } from "../Types/goTypes"; + +function DisplayUserProject(): JSX.Element { + const { projectName } = useParams(); + const [unsignedReports, setUnsignedReports] = useState([]); + const [usernames, setUsernames] = useState([]); + const token = localStorage.getItem("accessToken") ?? ""; + + 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 ( + <> +

+ All Unsigned Reports In: {projectName}{" "} +

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

+ +
+
+
+

+ ))} +
+ + ); +} + +export default DisplayUserProject; diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index f4fd782..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 { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; +import GetProjects from "./GetProjects"; import { api } from "../API/API"; /** @@ -9,33 +10,45 @@ import { api } from "../API/API"; */ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); + const navigate = useNavigate(); - const getProjects = async (): Promise => { + GetProjects({ + setProjectsProp: setProjects, + username: localStorage.getItem("username") ?? "", + }); + + const handleProjectClick = async (projectName: string): Promise => { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(token); - console.log(response); + const response = await api.checkIfProjectManager(projectName, token); + console.log(response.data); if (response.success) { - setProjects(response.data ?? []); + if ( + (response.data as unknown as { isProjectManager: boolean }) + .isProjectManager + ) { + navigate(`/PMProjectPage/${projectName}`); + } else { + navigate(`/project/${projectName}`); + } } else { + // handle error console.error(response.message); } }; - // Call getProjects when the component mounts - useEffect(() => { - void getProjects(); - }, []); - return ( <>

Your Projects

- {projects.map((project, index) => ( - + {projects.map((project) => ( +
void handleProjectClick(project.name)} + key={project.id} + >

{project.name}

- +
))}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index be96329..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"; @@ -18,47 +18,52 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const { projectName } = useParams(); - const { fetchedWeek } = useParams(); - - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - projectName ?? "", - fetchedWeek?.toString() ?? "0", - token, - ); - - if (response.success) { - const report: WeeklyReport = response.data ?? { - reportId: 0, - userId: 0, - projectId: 0, - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; + const { projectName, fetchedWeek } = useParams<{ + projectName: string; + fetchedWeek: string; + }>(); + const username = localStorage.getItem("userName") ?? ""; + console.log(projectName, fetchedWeek); useEffect(() => { - void fetchWeeklyReport(); - }); + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek ?? "", + token, + ); - const handleNewWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchWeeklyReport(); + }, [projectName, fetchedWeek, token]); + + const handleUpdateWeeklyReport = async (): Promise => { + const updateWeeklyReport: UpdateWeeklyReport = { + userName: username, projectName: projectName ?? "", week, developmentTime, @@ -69,13 +74,14 @@ export default function GetWeeklyReport(): JSX.Element { testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + await api.updateWeeklyReport(updateWeeklyReport, token); }; const navigate = useNavigate(); return ( <> +

Edit Time Report

{ @@ -85,29 +91,16 @@ export default function GetWeeklyReport(): JSX.Element { return; } e.preventDefault(); - void handleNewWeeklyReport(); + void handleUpdateWeeklyReport(); + alert("Changes submitted"); navigate(-1); }} >
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> +
+

Week: {week}

+
+ @@ -127,13 +120,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -146,13 +149,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -165,13 +178,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -184,13 +207,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -203,13 +236,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -222,13 +265,23 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} 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 a128b8d..ffc8b21 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -12,65 +12,103 @@ import Button from "./Button"; */ export default function NewWeeklyReport(): JSX.Element { const [week, setWeek] = useState(0); - const [developmentTime, setDevelopmentTime] = useState(); - const [meetingTime, setMeetingTime] = useState(); - const [adminTime, setAdminTime] = useState(); - const [ownWorkTime, setOwnWorkTime] = useState(); - const [studyTime, setStudyTime] = useState(); - const [testingTime, setTestingTime] = useState(); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); const { projectName } = useParams(); const token = localStorage.getItem("accessToken") ?? ""; - const handleNewWeeklyReport = async (): Promise => { + const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { projectName: projectName ?? "", week: week, - developmentTime: developmentTime ?? 0, - meetingTime: meetingTime ?? 0, - adminTime: adminTime ?? 0, - ownWorkTime: ownWorkTime ?? 0, - studyTime: studyTime ?? 0, - testingTime: testingTime ?? 0, + developmentTime: developmentTime, + meetingTime: meetingTime, + adminTime: adminTime, + ownWorkTime: ownWorkTime, + studyTime: studyTime, + testingTime: testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + const response = await api.submitWeeklyReport(newWeeklyReport, token); + console.log(response); + if (response.success) { + return true; + } else { + return false; + } }; const navigate = useNavigate(); + // Check if the browser is Chrome or Edge + const isChromeOrEdge = /Chrome|Edg/.test(navigator.userAgent); return ( <>
{ - if (week === 0) { - alert("Please enter a week number"); - e.preventDefault(); - return; - } e.preventDefault(); - void handleNewWeeklyReport(); - navigate(-1); + void (async (): Promise => { + if (week === 0 || week > 53 || week < 1) { + alert("Please enter a valid week number"); + return; + } + + const success = await handleNewWeeklyReport(); + if (!success) { + alert( + "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; + } + alert("Weekly report submitted successfully"); + navigate(-1); + })(); }} >
- { - setWeek(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + {isChromeOrEdge ? ( + { + const weekNumber = parseInt(e.target.value.split("-W")[1]); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> + }} + /> + ) : ( + { + const weekNumber = parseInt(e.target.value); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + )}
@@ -90,13 +128,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -109,13 +157,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -128,13 +186,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -147,13 +215,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -166,13 +244,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + if ( + !/\d/.test(keyValue) && + keyValue !== "Backspace" && + keyValue !== "ArrowLeft" && + keyValue !== "ArrowRight" + ) event.preventDefault(); }} /> @@ -185,13 +273,23 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} 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 new file mode 100644 index 0000000..ce7761c --- /dev/null +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -0,0 +1,160 @@ +import { useState, useEffect } from "react"; +import { WeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ + +//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project. +export default function OtherUsersTR(): JSX.Element { + const [week, setWeek] = useState(0); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); + + const token = localStorage.getItem("accessToken") ?? ""; + const { projectName } = useParams(); + const { username } = useParams(); + const { fetchedWeek } = useParams(); + + useEffect(() => { + const fetchUsersWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + username ?? "", + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchUsersWeeklyReport(); + }); + + return ( + <> +

{username}'s Report

+
+
+
+

Week: {week}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Activity + Total Time (min) +
Development + +
Meeting + +
Administration + +
Own Work + +
Studies + +
Testing + +
+
+
+ + ); +} diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index b153e9c..1f98d79 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -1,27 +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 (
-
-
-

Project members:

-
+
+
+

+ {props.projectname} +

+
+

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) => ( @@ -41,22 +68,44 @@ function ProjectInfoModal(props: { ))}
-
-
-
diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index 4ebdaf8..294a131 100644 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -1,8 +1,7 @@ import { useState } from "react"; import { NewProject } from "../Types/goTypes"; import ProjectInfoModal from "./ProjectInfoModal"; -import UserInfoModal from "./UserInfoModal"; -import DeleteUser from "./DeleteUser"; +import MemberInfoModal from "./MemberInfoModal"; /** * A list of projects for admin manage projects page, that sets an onClick @@ -19,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,37 +27,37 @@ export function ProjectListAdmin(props: { setUserModalVisible(true); }; - const handleClickProject = (username: string): void => { - setProjectname(username); + const handleClickProject = (projectname: string): void => { + setProjectName(projectname); setProjectModalVisible(true); }; const handleCloseProject = (): void => { - setProjectname(""); + setProjectName(""); setProjectModalVisible(false); }; const handleCloseUser = (): void => { - setProjectname(""); + setUsername(""); setUserModalVisible(false); }; return ( <> - - DeleteUser} - 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 73e29e5..52e8559 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,91 +1,39 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Link, useParams } from "react-router-dom"; +import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); const [projectMembers, setProjectMembers] = useState([]); - // const getProjectMembers = async (): Promise => { - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.getProjectMembers(projectName ?? "", token); - // console.log(response); - // if (response.success) { - // setProjectMembers(response.data ?? []); - // } else { - // console.error(response.message); - // } - // }; - - interface ProjectMember { - username: string; - role: string; - } - - const mockProjectMembers = [ - { - username: "username1", - role: "Project Manager", - }, - { - username: "username2", - role: "System Manager", - }, - { - username: "username3", - role: "Developer", - }, - { - username: "username4", - role: "Tester", - }, - { - username: "username5", - role: "Tester", - }, - { - username: "username6", - role: "Tester", - }, - ]; - - const getProjectMembers = async (): Promise => { - // Use the mock data - setProjectMembers(mockProjectMembers); - - await Promise.resolve(); - }; - - useEffect(() => { - void getProjectMembers(); + GetUsersInProject({ + projectName: projectName ?? "", + setUsersProp: setProjectMembers, }); return ( <> +

    + All Members In: {projectName}{" "} +

    - {projectMembers.map((projectMember, index) => ( + {projectMembers.map((projectMember: ProjectMember, index: number) => (

    -

    {projectMember.username}

    +

    {projectMember.Username}

    Role: -

    {projectMember.role}

    +

    {projectMember.UserRole}

    View Reports

    - -

    - Change Role -

    -
    diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index df07c6e..be35a74 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -4,7 +4,6 @@ import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; import InputField from "./InputField"; -import { useNavigate } from "react-router-dom"; /** * Renders a registration form for the admin to add new users in. @@ -15,17 +14,22 @@ export default function Register(): JSX.Element { const [password, setPassword] = useState(); const [errMessage, setErrMessage] = useState(); - const nav = useNavigate(); - 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) { - nav("/"); // Instantly navigate to the login page + alert(`${newUser.username} added!`); + setPassword(""); + setUsername(""); } else { + alert("User not added, name could be taken"); setErrMessage(response.message ?? "Unknown error"); console.error(errMessage); } @@ -49,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 new file mode 100644 index 0000000..3689854 --- /dev/null +++ b/frontend/src/Components/ViewOtherTimeReport.tsx @@ -0,0 +1,200 @@ +import { useState, useEffect } from "react"; +import { WeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useNavigate, useParams } from "react-router-dom"; +import Button from "./Button"; + +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ + +//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project. +export default function GetOtherUsersReport(): JSX.Element { + const [week, setWeek] = useState(0); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + 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(); + const { username } = useParams(); + const { fetchedWeek } = useParams(); + + useEffect(() => { + const fetchUsersWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + username ?? "", + ); + console.log(response); + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setReportId(report.reportId); + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchUsersWeeklyReport(); + }); + + 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

      +
      + { + e.preventDefault(); + void (async (): Promise => { + const success = await handleSignWeeklyReport(); + if (!success) { + alert("Failed to sign report!"); + return; + } + alert("Report successfully signed!"); + navigate(-1); + })(); + }} + > +
      +
      +

      Week: {week}

      +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + Activity + + Total Time (min) +
      Development + +
      Meeting + +
      Administration + +
      Own Work + +
      Studies + +
      Testing + +
      +
      + +
      + + ); +} 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 = ( - <> -