diff --git a/.gitignore b/.gitignore index bdbfff8..313b735 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ dist/ .vscode/ .idea/ .DS_Store +.go.work.sum # Ignore configuration files .env diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index ef365cd..e2aa366 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,7 +2,7 @@ package database import ( "embed" - "os" + "errors" "path/filepath" "ttime/internal/types" @@ -19,7 +19,7 @@ type Database interface { PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error - Migrate(dirname string) error + Migrate() error GetProjectId(projectname string) (int, error) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error @@ -30,6 +30,9 @@ type Database interface { GetAllProjects() ([]types.Project, error) GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) + GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) + SignWeeklyReport(reportId int, projectManagerId int) error + IsSiteAdmin(username string) (bool, error) } // This struct is a wrapper type that holds the database connection @@ -104,7 +107,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { // GetProject retrieves a specific project by its ID. func (d *Db) GetProject(projectId int) (types.Project, error) { var project types.Project - err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") + err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId) + if err != nil { + println("Error getting project: ", err) + } return project, err } @@ -257,15 +263,94 @@ func (d *Db) GetAllUsersApplication() ([]string, error) { return usernames, nil } +func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) { + var report types.WeeklyReport + 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 + user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + AND week = ? + ` + err := d.Get(&report, query, username, projectName, week) + return report, err +} + +// SignWeeklyReport signs a weekly report by updating the signed_by field +// with the provided project manager's ID, but only if the project manager +// is in the same project as the report +func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { + // Retrieve the project ID associated with the report + var reportProjectID int + err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId) + if err != nil { + return err + } + + // 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) + if err != nil { + return err + } + + // Check if the project manager is in the same project as the report + if reportProjectID != managerProjectID { + return errors.New("project manager doesn't have permission to sign the report") + } + + // Update the signed_by field of the specified report + _, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId) + return err +} + +// IsSiteAdmin checks if a given username is a site admin +func (d *Db) IsSiteAdmin(username string) (bool, error) { + // Define the SQL query to check if the user is a site admin + query := ` + SELECT COUNT(*) FROM site_admin + JOIN users ON site_admin.admin_id = users.id + WHERE users.username = ? + ` + + // Execute the query + var count int + err := d.Get(&count, query, username) + if err != nil { + return false, err + } + + // If count is greater than 0, the user is a site admin + return count > 0, nil +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory -func (d *Db) Migrate(dirname string) error { +func (d *Db) Migrate() error { // Read the embedded scripts directory files, err := scripts.ReadDir("migrations") if err != nil { return err } + if len(files) == 0 { + println("No migration files found") + return nil + } + tr := d.MustBegin() // Iterate over each SQL file and execute it @@ -275,8 +360,7 @@ func (d *Db) Migrate(dirname string) error { } // This is perhaps not the most elegant way to do this - sqlFile := filepath.Join("migrations", file.Name()) - sqlBytes, err := os.ReadFile(sqlFile) + sqlBytes, err := scripts.ReadFile("migrations/" + file.Name()) if err != nil { return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 5438d66..a7f3878 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,6 +1,7 @@ package database import ( + "fmt" "testing" ) @@ -8,7 +9,7 @@ import ( func setupState() (Database, error) { db := DbConnect(":memory:") - err := db.Migrate("../../migrations") + err := db.Migrate() if err != nil { return nil, err } @@ -371,3 +372,197 @@ func TestAddProject(t *testing.T) { t.Error("Added project not found") } } + +func TestGetWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "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) + } + + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Check if the retrieved report matches the expected values + if report.UserId != 1 { + t.Errorf("Expected UserId to be 1, got %d", report.UserId) + } + if report.ProjectId != 1 { + t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId) + } + if report.Week != 1 { + t.Errorf("Expected Week to be 1, got %d", report.Week) + } + // Check other fields similarly +} + +func TestSignWeeklyReport(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add both regular users as members to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("projectManager", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for one of the regular users + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Print project manager's ID + projectManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + fmt.Println("Project Manager's ID:", projectManagerID) + + // Sign the report with the project manager + err = db.SignWeeklyReport(report.ReportId, projectManagerID) + if err != nil { + t.Error("SignWeeklyReport failed:", err) + } + + // Retrieve the report again to check if it's signed + signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Ensure the report is signed by the project manager + if *signedReport.SignedBy != projectManagerID { + t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy) + } +} + +func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add project manager + err = db.AddUser("projectManager", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a regular user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add project + err = db.AddProject("testproject", "description", "projectManager") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Add the regular user as a member to the project + err = db.AddUserToProject("testuser", "testproject", "member") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + // Add a weekly report for the regular user + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Retrieve the added report + report, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + anotherManagerID, err := db.GetUserId("projectManager") + if err != nil { + t.Error("GetUserId failed:", err) + } + + err = db.SignWeeklyReport(report.ReportId, anotherManagerID) + if err == nil { + t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") + } +} + +func TestGetProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Retrieve the added project + project, err := db.GetProject(1) + if err != nil { + t.Error("GetProject failed:", err) + } + + // Check if the retrieved project matches the expected values + if project.Name != "testproject" { + t.Errorf("Expected Name to be testproject, got %s", project.Name) + } +} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index 5c9d329..d2e2dd1 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -3,7 +3,7 @@ -- username is what is used for login -- password is the hashed password CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index 58d8e97..99bce86 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS projects ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, owner_user_id INTEGER NOT NULL, diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index 0e29b97..8f76b80 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -1,14 +1,16 @@ -CREATE TABLE weekly_reports ( - user_id INTEGER, - project_id INTEGER, - week INTEGER, +CREATE TABLE IF NOT EXISTS weekly_reports ( + report_id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + week INTEGER NOT NULL, development_time INTEGER, meeting_time INTEGER, admin_time INTEGER, own_work_time INTEGER, study_time INTEGER, testing_time INTEGER, + signed_by INTEGER, FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (project_id) REFERENCES projects(id) - PRIMARY KEY (user_id, project_id, week) -) \ No newline at end of file + FOREIGN KEY (project_id) REFERENCES projects(id), + FOREIGN KEY (signed_by) REFERENCES users(id) +); \ No newline at end of file diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 2378f7b..932451d 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,13 +1,9 @@ package handlers import ( - "strconv" - "time" "ttime/internal/database" - "ttime/internal/types" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" ) // The actual interface that we will use @@ -19,6 +15,11 @@ type GlobalState interface { 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 // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project @@ -33,251 +34,17 @@ type GlobalState interface { // UpdateCollection(c *fiber.Ctx) error // To update a collection // DeleteCollection(c *fiber.Ctx) error // To delete a collection // SignCollection(c *fiber.Ctx) error // To sign a collection - GetButtonCount(c *fiber.Ctx) error // For demonstration purposes - IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes - 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 + 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 } // "Constructor" func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db, ButtonCount: 0} + return &GState{Db: db} } // The global state, which implements all the handlers type GState struct { - Db database.Database - ButtonCount int -} - -// Register is a simple handler that registers a new user -// -// @Summary Register a new user -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce json -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /api/register [post] -func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - return c.Status(400).SendString(err.Error()) - } - - if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("User added") -} - -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -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 { - return c.Status(403).SendString("You can only delete yourself") - } - - if err := gs.Db.RemoveUser(username); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("User deleted") -} - -func (gs *GState) GetButtonCount(c *fiber.Ctx) error { - return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) -} - -func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { - gs.ButtonCount++ - return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) -} - -// Login is a simple login handler that returns a JWT token -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 { - return c.Status(400).SendString(err.Error()) - } - - if !gs.Db.CheckUser(u.Username, u.Password) { - println("User not found") - return c.SendStatus(fiber.StatusUnauthorized) - } - - // Create the Claims - claims := jwt.MapClaims{ - "name": u.Username, - "admin": false, - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - - return c.JSON(fiber.Map{"token": t}) -} - -// LoginRenew is a simple handler that renews the token -func (gs *GState) LoginRenew(c *fiber.Ctx) error { - // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " - user := c.Locals("user").(*jwt.Token) - 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 { - return c.SendStatus(fiber.StatusInternalServerError) - } - return c.JSON(fiber.Map{"token": t}) -} - -// 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") -} - -// 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) -} - -// ListAllUsers is a handler that returns a list of all users in the application database -func (gs *GState) ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := gs.Db.GetAllUsersApplication() - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} - -func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { - // Extract the project name from the request parameters or body - projectName := c.Params("projectName") - - // Get all users associated with the project from the database - users, err := gs.Db.GetAllUsersProject(projectName) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} - -// ProjectRoleChange is a handler that changes a user's role within a project -func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { - // Extract the necessary parameters from the request - username := c.Params("username") - projectName := c.Params("projectName") - role := c.Params("role") - - // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, projectName, 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") - - // Parse the project ID into an integer - projectIDInt, err := strconv.Atoi(projectID) - if err != nil { - 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 { - return c.Status(500).SendString(err.Error()) - } - - // Return the project as JSON - return c.JSON(project) -} - -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 { - return c.Status(400).SendString(err.Error()) - } - - // Make sure all the fields of the report are valid - if report.Week < 1 || report.Week > 52 { - 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 { - 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 { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Time report added") + Db database.Database } diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go new file mode 100644 index 0000000..3732249 --- /dev/null +++ b/backend/internal/handlers/handlers_project_related.go @@ -0,0 +1,145 @@ +package handlers + +import ( + "strconv" + "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 (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") +} + +// 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 { + // Extract the necessary parameters from the request + username := c.Params("username") + projectName := c.Params("projectName") + role := c.Params("role") + + // Change the user's role within the project in the database + if err := gs.Db.ChangeUserRole(username, projectName, 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 == "" { + return c.Status(400).SendString("No project ID provided") + } + println("Getting project with ID: ", projectID) + + // Parse the project ID into an integer + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + 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 { + return c.Status(500).SendString(err.Error()) + } + + // Return the project as JSON + println("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") + + // Get all users associated with the project from the database + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +// AddUserToProjectHandler is a handler that adds a user to a project with a specified role +func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { + // Extract necessary parameters from the request + var requestData struct { + Username string `json:"username"` + ProjectName string `json:"projectName"` + Role string `json:"role"` + } + if err := c.BodyParser(&requestData); err != nil { + println("Error parsing request body:", err) + return c.Status(400).SendString("Bad request") + } + + // Check if the user adding another user to the project is a site admin + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + println("Admin username from claims:", adminUsername) + + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + println("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + println("User is not a site admin:", adminUsername) + return c.Status(403).SendString("User is not a site admin") + } + + // Add the user to the project with the specified role + err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + if err != nil { + println("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + println("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go new file mode 100644 index 0000000..291d068 --- /dev/null +++ b/backend/internal/handlers/handlers_report_related.go @@ -0,0 +1,105 @@ +package handlers + +import ( + "strconv" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "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 { + return c.Status(400).SendString(err.Error()) + } + + // Make sure all the fields of the report are valid + if report.Week < 1 || report.Week > 52 { + 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 { + 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 { + return c.Status(500).SendString(err.Error()) + } + + 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 + println("GetWeeklyReport") + 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") + println(projectName) + week := c.Query("week") + println(week) + + // Convert week to integer + weekInt, err := strconv.Atoi(week) + if err != nil { + 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 { + return c.Status(500).SendString(err.Error()) + } + + // Return the retrieved weekly report + return c.JSON(report) +} + +type ReportId struct { + ReportId int +} + +func (gs *GState) SignReport(c *fiber.Ctx) error { + println("Signing report...") + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + projectManagerUsername := claims["name"].(string) + + // Extract report ID from the request query parameters + // reportID := c.Query("reportId") + rid := new(ReportId) + if err := c.BodyParser(rid); err != nil { + return err + } + println("Signing report for: ", rid.ReportId) + // reportIDInt, err := strconv.Atoi(rid.ReportId) + // println("Signing report for: ", rid.ReportId) + // if err != nil { + // return c.Status(400).SendString("Invalid report ID") + // } + + // Get the project manager's ID + projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) + if err != nil { + return c.Status(500).SendString("Failed to get project manager ID") + } + println("blabla", projectManagerID) + + // Call the database function to sign the weekly report + err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Weekly report signed successfully") +} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go new file mode 100644 index 0000000..0f7c047 --- /dev/null +++ b/backend/internal/handlers/handlers_user_related.go @@ -0,0 +1,147 @@ +package handlers + +import ( + "fmt" + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// Register is a simple handler that registers a new user +// +// @Summary Register a new user +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /api/register [post] +func (gs *GState) Register(c *fiber.Ctx) error { + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + println("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + println("Adding user:", u.Username) + if err := gs.Db.AddUser(u.Username, u.Password); err != nil { + return c.Status(500).SendString(err.Error()) + } + + println("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 +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 { + return c.Status(403).SendString("You can only delete yourself") + } + + if err := gs.Db.RemoveUser(username); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User deleted") +} + +// Login is a simple login handler that returns a JWT token +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 { + println("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + println("Username:", u.Username) + if !gs.Db.CheckUser(u.Username, u.Password) { + println("User not found") + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": u.Username, + "admin": false, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + println("Token created for user:", u.Username) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + println("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) + } + + println("Successfully signed token for user:", u.Username) + return c.JSON(fiber.Map{"token": t}) +} + +// LoginRenew is a simple handler that renews the token +func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " + user := c.Locals("user").(*jwt.Token) + 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 { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"token": t}) +} + +// ListAllUsers is a handler that returns a list of all users in the application database +func (gs *GState) ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := gs.Db.GetAllUsersApplication() + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { + // Extract the username from the request body + var newUser types.NewUser + if err := c.BodyParser(&newUser); err != nil { + return c.Status(400).SendString("Bad request") + } + username := newUser.Username + + println("Promoting user to admin:", username) // Debug print + + // Promote the user to a site admin in the database + if err := gs.Db.PromoteToAdmin(username); err != nil { + fmt.Println("Error promoting user to admin:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + println("User promoted to admin successfully:", username) // Debug print + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index e0ea1ef..299395a 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -19,3 +19,28 @@ type NewWeeklyReport struct { // Total time spent on testing TestingTime int `json:"testingTime"` } + +type WeeklyReport struct { + // The ID of the report + ReportId int `json:"reportId" db:"report_id"` + // The user id of the user who submitted the report + UserId int `json:"userId" db:"user_id"` + // The name of the project, as it appears in the database + ProjectId int `json:"projectId" db:"project_id"` + // The week number + Week int `json:"week" db:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime" db:"development_time"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime" db:"meeting_time"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime" db:"admin_time"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"` + // Total time spent on studying + StudyTime int `json:"studyTime" db:"study_time"` + // Total time spent on testing + TestingTime int `json:"testingTime" db:"testing_time"` + // The project manager who signed it + SignedBy *int `json:"signedBy" db:"signed_by"` +} diff --git a/backend/main.go b/backend/main.go index 9ba2556..da89043 100644 --- a/backend/main.go +++ b/backend/main.go @@ -43,6 +43,11 @@ 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) + } + // Get our global state gs := handlers.NewGlobalState(db) // Create the server @@ -56,11 +61,6 @@ func main() { // Register our unprotected routes server.Post("/api/register", gs.Register) - - // Register handlers for example button count - server.Get("/api/button", gs.GetButtonCount) - server.Post("/api/button", gs.IncrementButtonCount) - server.Post("/api/login", gs.Login) // Every route from here on will require a valid JWT @@ -68,11 +68,17 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) + // Protected routes (require a valid JWT bearer token authentication header) server.Post("/api/submitReport", 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.Post("/api/project", gs.CreateProject) + server.Get("/api/project/:projectId", gs.GetProject) + server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) + server.Post("/api/signReport", gs.SignReport) + server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) + server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 248ad37..6078513 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,8 +1,13 @@ -import { NewProject, Project } from "../Types/Project"; -import { NewUser, User } from "../Types/Users"; +import { + NewWeeklyReport, + NewUser, + User, + Project, + NewProject, +} from "../Types/goTypes"; // This type of pattern should be hard to misuse -interface APIResponse { +export interface APIResponse { success: boolean; message?: string; data?: T; @@ -15,13 +20,34 @@ interface API { registerUser(user: NewUser): Promise>; /** Remove a user */ removeUser(username: string, token: string): Promise>; + /** Login */ + login(NewUser: NewUser): Promise>; + /** Renew the token */ + renewToken(token: string): Promise>; /** Create a project */ createProject( project: NewProject, token: string, ): Promise>; - /** Renew the token */ - renewToken(token: string): Promise>; + /** Submit a weekly report */ + submitWeeklyReport( + project: NewWeeklyReport, + token: string, + ): Promise>; + /**Gets a weekly report*/ + getWeeklyReport( + username: string, + projectName: string, + week: string, + token: string, + ): Promise>; + /** Gets all the projects of a user*/ + getUserProjects( + username: string, + token: string, + ): Promise>; + /** Gets a project from id*/ + getProject(id: number): Promise>; } // Export an instance of the API @@ -37,13 +63,19 @@ export const api: API = { }); if (!response.ok) { - return { success: false, message: "Failed to register user" }; + return { + success: false, + message: "Failed to register user: " + response.status, + }; } else { - const data = (await response.json()) as User; - return { success: true, data }; + // const data = (await response.json()) as User; // The API does not currently return the user + return { success: true }; } } catch (e) { - return { success: false, message: "Failed to register user" }; + return { + success: false, + message: "Unknown error while registering user", + }; } }, @@ -117,4 +149,140 @@ export const api: API = { return { success: false, message: "Failed to renew token" }; } }, + + async getUserProjects( + username: string, + token: string, + ): Promise> { + try { + const response = await fetch("/api/getUserProjects", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify({ username }), + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to get user projects", + }); + } else { + const data = (await response.json()) as Project[]; + return Promise.resolve({ success: true, data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to get user projects", + }); + } + }, + + async submitWeeklyReport( + weeklyReport: NewWeeklyReport, + token: string, + ): Promise> { + try { + const response = await fetch("/api/submitWeeklyReport", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify(weeklyReport), + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } + + const data = (await response.json()) as NewWeeklyReport; + return { success: true, data }; + } catch (e) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } + }, + + async getWeeklyReport( + username: string, + projectName: string, + week: string, + token: string, + ): Promise> { + try { + const response = await fetch("/api/getWeeklyReport", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + body: JSON.stringify({ username, projectName, week }), + }); + + if (!response.ok) { + return { success: false, message: "Failed to get weekly report" }; + } else { + const data = (await response.json()) as NewWeeklyReport; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to get weekly report" }; + } + }, + + async login(NewUser: NewUser): Promise> { + try { + const response = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(NewUser), + }); + + if (!response.ok) { + return { success: false, message: "Failed to login" }; + } else { + const data = (await response.json()) as { token: string }; // Update the type of 'data' + return { success: true, data: data.token }; + } + } catch (e) { + return Promise.resolve({ success: false, message: "Failed to login" }); + } + }, + + // Gets a projet by id, currently untested since we have no javascript-based tests + async getProject(id: number): Promise> { + try { + const response = await fetch(`/api/project/${id}`, { + method: "GET", + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to get project: Response code " + response.status, + }; + } else { + const data = (await response.json()) as Project; + return { success: true, data }; + } + // The code below is garbage but satisfies the linter + // This needs fixing, do not copy this pattern + } catch (e: unknown) { + return { + success: false, + message: "Failed to get project: " + (e as Error).toString(), + }; + } + }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx new file mode 100644 index 0000000..45814e3 --- /dev/null +++ b/frontend/src/Components/AddProject.tsx @@ -0,0 +1,94 @@ +import { useState } from "react"; +import { APIResponse, api } from "../API/API"; +import { NewProject, Project } 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 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; +} + +/** + * Tries to add a project to the system + * @returns {JSX.Element} UI for project adding + */ +function AddProject(): JSX.Element { + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + return ( +
+
+
{ + e.preventDefault(); + CreateProject({ name: name, description: description }); + }} + > + TTIME Logo +

+ Create a new project +

+ { + setName(e.target.value); + }} + /> + { + setDescription(e.target.value); + }} + /> +
+
+ +

+
+
+ ); +} + +export default AddProject; diff --git a/frontend/src/Components/BackButton.tsx b/frontend/src/Components/BackButton.tsx new file mode 100644 index 0000000..7a1ac81 --- /dev/null +++ b/frontend/src/Components/BackButton.tsx @@ -0,0 +1,18 @@ +import { useNavigate } from "react-router-dom"; + +function BackButton(): JSX.Element { + const navigate = useNavigate(); + const goBack = (): void => { + navigate(-1); + }; + return ( + + ); +} + +export default BackButton; diff --git a/frontend/src/Components/BackgroundAnimation.tsx b/frontend/src/Components/BackgroundAnimation.tsx new file mode 100644 index 0000000..5f402c0 --- /dev/null +++ b/frontend/src/Components/BackgroundAnimation.tsx @@ -0,0 +1,24 @@ +import { useEffect } from "react"; + +const BackgroundAnimation = (): JSX.Element => { + useEffect(() => { + const images = [ + "src/assets/1.jpg", + "src/assets/2.jpg", + "src/assets/3.jpg", + "src/assets/4.jpg", + ]; + + // Pre-load images + for (const i of images) { + console.log(i); + } + + // Start animation + document.body.style.animation = "backgroundTransition 30s infinite"; + }, []); + + return <>; +}; + +export default BackgroundAnimation; diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx index 1835d6a..d5fd3b6 100644 --- a/frontend/src/Components/BasicWindow.tsx +++ b/frontend/src/Components/BasicWindow.tsx @@ -2,17 +2,15 @@ import Header from "./Header"; import Footer from "./Footer"; function BasicWindow({ - username, content, buttons, }: { - username: string; content: React.ReactNode; buttons: React.ReactNode; }): JSX.Element { return (
-
+
{content}
{buttons}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx new file mode 100644 index 0000000..b0e8771 --- /dev/null +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -0,0 +1,247 @@ +import { useState, useEffect } from "react"; +import { NewWeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useNavigate } from "react-router-dom"; +import Button from "./Button"; + +export default function GetWeeklyReport(): JSX.Element { + const [projectName, setProjectName] = useState(""); + 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 username = localStorage.getItem("username") ?? ""; + + useEffect(() => { + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + username, + projectName, + week.toString(), + token, + ); + + if (response.success) { + const report: NewWeeklyReport = response.data ?? { + projectName: "", + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setProjectName(report.projectName); + 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, token, username, week]); + + const handleNewWeeklyReport = async (): Promise => { + const newWeeklyReport: NewWeeklyReport = { + projectName, + week, + developmentTime, + meetingTime, + adminTime, + ownWorkTime, + studyTime, + testingTime, + }; + + await api.submitWeeklyReport(newWeeklyReport, token); + }; + + const navigate = useNavigate(); + + return ( + <> +
+
{ + if (week === 0) { + alert("Please enter a week number"); + e.preventDefault(); + return; + } + e.preventDefault(); + void handleNewWeeklyReport(); + navigate("/project"); + }} + > +
+ { + const weekNumber = parseInt(e.target.value.split("-W")[1]); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Activity + + Total Time (min) +
Development + { + setDevelopmentTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
Meeting + { + setMeetingTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
Administration + { + setAdminTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
Own Work + { + setOwnWorkTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
Studies + { + setStudyTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
Testing + { + setTestingTime(parseInt(e.target.value)); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
+
+
+
+ + ); +} diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index ba0a939..819c5de 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -function Header({ username }: { username: string }): JSX.Element { +function Header(): JSX.Element { const [isOpen, setIsOpen] = useState(false); const handleLogout = (): void => { - // Add any logout logic here + localStorage.clear(); }; return ( @@ -31,7 +31,7 @@ function Header({ username }: { username: string }): JSX.Element { }} > {isOpen && ( diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx new file mode 100644 index 0000000..639b4ca --- /dev/null +++ b/frontend/src/Components/InputField.tsx @@ -0,0 +1,41 @@ +/** + * A customizable input field + * @param props - Settings for the field + * @returns {JSX.Element} The input field + * @example + * { + * setExample(e.target.value); + * }} + * value={example} + * /> + */ +function InputField(props: { + label: string; + type: string; + value: string; + onChange: (e: React.ChangeEvent) => void; +}): JSX.Element { + return ( +
+ + +
+ ); +} + +export default InputField; diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx new file mode 100644 index 0000000..ce7d52c --- /dev/null +++ b/frontend/src/Components/LoginCheck.tsx @@ -0,0 +1,58 @@ +import { NewUser } from "../Types/goTypes"; +import { api, APIResponse } from "../API/API"; +import { Dispatch, SetStateAction } from "react"; + +/* + * Checks if user is in database with api.login and then sets proper authority level + * TODO: change so that it checks for user type (admin, user, pm) somehow instead + **/ +function LoginCheck(props: { + username: string; + password: string; + setAuthority: Dispatch>; +}): void { + const user: NewUser = { + username: props.username, + password: props.password, + }; + + localStorage.clear(); + + api + .login(user) + .then((response: APIResponse) => { + if (response.success) { + if (response.data !== undefined) { + const token = response.data; + localStorage.setItem("accessToken", token); + localStorage.setItem("username", props.username); + //TODO: change so that it checks for user type (admin, user, pm) instead + if (token !== "" && props.username === "admin") { + props.setAuthority((prevAuth) => { + prevAuth = 1; + return prevAuth; + }); + } else if (token !== "" && props.username === "pm") { + props.setAuthority((prevAuth) => { + prevAuth = 2; + return prevAuth; + }); + } else if (token !== "" && props.username === "user") { + props.setAuthority((prevAuth) => { + prevAuth = 3; + return prevAuth; + }); + } + } else { + console.error("Token was undefined"); + } + } else { + console.error("Token could not be fetched/No such user"); + } + }) + .catch((error) => { + console.error("An error occurred during login:", error); + }); +} + +export default LoginCheck; diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx new file mode 100644 index 0000000..d7768c0 --- /dev/null +++ b/frontend/src/Components/LoginField.tsx @@ -0,0 +1,55 @@ +import { Dispatch, FormEventHandler, SetStateAction } from "react"; +import Button from "./Button"; +import InputField from "./InputField"; + +/** + * A login field complete with input fields + * and a button for submitting the information + * @param props - Settings + * @returns {JSX.Element} A login component + * @example + * + */ +function Login(props: { + handleSubmit: FormEventHandler; + setUsername: Dispatch>; + setPassword: Dispatch>; + username: string; + password: string; +}): JSX.Element { + return ( +
+ { + props.setUsername(e.target.value); + }} + value={props.username} + /> + { + props.setPassword(e.target.value); + }} + value={props.password} + /> +