diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5cbb13f..ef365cd 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,7 +2,7 @@ package database import ( "embed" - "errors" + "os" "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() error + Migrate(dirname string) 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,8 +30,6 @@ 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 } // This struct is a wrapper type that holds the database connection @@ -259,74 +257,15 @@ 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 -} - // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory -func (d *Db) Migrate() error { +func (d *Db) Migrate(dirname string) 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 @@ -336,7 +275,8 @@ func (d *Db) Migrate() error { } // This is perhaps not the most elegant way to do this - sqlBytes, err := scripts.ReadFile("migrations/" + file.Name()) + sqlFile := filepath.Join("migrations", file.Name()) + sqlBytes, err := os.ReadFile(sqlFile) if err != nil { return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 09de45b..5438d66 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,7 +1,6 @@ package database import ( - "fmt" "testing" ) @@ -9,7 +8,7 @@ import ( func setupState() (Database, error) { db := DbConnect(":memory:") - err := db.Migrate() + err := db.Migrate("../../migrations") if err != nil { return nil, err } @@ -372,167 +371,3 @@ 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") - } -} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index d2e2dd1..5c9d329 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 AUTOINCREMENT, + id INTEGER PRIMARY KEY, 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 99bce86..58d8e97 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 AUTOINCREMENT, + id INTEGER PRIMARY KEY, 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 8f76b80..0e29b97 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -1,16 +1,14 @@ -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, +CREATE TABLE weekly_reports ( + user_id INTEGER, + project_id INTEGER, + week INTEGER, 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), - FOREIGN KEY (signed_by) REFERENCES users(id) -); \ No newline at end of file + FOREIGN KEY (project_id) REFERENCES projects(id) + PRIMARY KEY (user_id, project_id, week) +) \ No newline at end of file diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 57a1969..2378f7b 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,9 +1,13 @@ 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 @@ -15,7 +19,6 @@ 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 // 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 @@ -48,6 +51,50 @@ type GState struct { 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}) } @@ -56,3 +103,181 @@ 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") +} diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go deleted file mode 100644 index 6a430e9..0000000 --- a/backend/internal/handlers/handlers_project_related.go +++ /dev/null @@ -1,98 +0,0 @@ -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") - - // 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) 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) -} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go deleted file mode 100644 index 506225b..0000000 --- a/backend/internal/handlers/handlers_report_related.go +++ /dev/null @@ -1,65 +0,0 @@ -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) -} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go deleted file mode 100644 index 0619ea5..0000000 --- a/backend/internal/handlers/handlers_user_related.go +++ /dev/null @@ -1,124 +0,0 @@ -package handlers - -import ( - "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) -} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 299395a..e0ea1ef 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -19,28 +19,3 @@ 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 bc33942..9ba2556 100644 --- a/backend/main.go +++ b/backend/main.go @@ -43,11 +43,6 @@ 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 @@ -78,7 +73,6 @@ func main() { server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) - server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) // 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 ac0f531..a30151c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,13 +1,9 @@ -import { - NewWeeklyReport, - NewUser, - User, - Project, - NewProject, -} from "../Types/goTypes"; +import { NewProject, Project } from "../Types/Project"; +import { NewUser, User } from "../Types/Users"; +import { NewWeeklyReport } from "../Types/goTypes"; // This type of pattern should be hard to misuse -export interface APIResponse { +interface APIResponse { success: boolean; message?: string; data?: T; @@ -20,10 +16,6 @@ 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, @@ -33,19 +25,16 @@ interface API { submitWeeklyReport( project: NewWeeklyReport, token: string, - ): Promise>; - /**Gets a weekly report*/ - getWeeklyReport( - username: string, - projectName: string, - week: string, - token: string, - ): Promise>; + ): Promise>; + /** Renew the token */ + renewToken(token: string): Promise>; /** Gets all the projects of a user*/ getUserProjects( username: string, token: string, ): Promise>; + /** Login */ + login(NewUser: NewUser): Promise>; } // Export an instance of the API @@ -61,19 +50,13 @@ export const api: API = { }); if (!response.ok) { - return { - success: false, - message: "Failed to register user: " + response.status, - }; + return { success: false, message: "Failed to register user" }; } else { - // const data = (await response.json()) as User; // The API does not currently return the user - return { success: true }; + const data = (await response.json()) as User; + return { success: true, data }; } } catch (e) { - return { - success: false, - message: "Unknown error while registering user", - }; + return { success: false, message: "Failed to register user" }; } }, @@ -176,64 +159,40 @@ export const api: API = { }, async submitWeeklyReport( - weeklyReport: NewWeeklyReport, + project: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { - const response = await fetch("/api/submitWeeklyReport", { + return 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 }; + body: JSON.stringify(project), + }) + .then((response) => { + if (!response.ok) { + return { + success: false, + message: "Failed to submit weekly report", + }; + } else { + return response.json(); + } + }) + .then((data: Project) => { + return { success: true, data }; + }); } catch (e) { - return { + return Promise.resolve({ 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> { + async login(NewUser: NewUser): Promise> { try { const response = await fetch("/api/login", { method: "POST", @@ -246,8 +205,8 @@ export const api: API = { 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 }; + const data = (await response.json()) as JSON; + return { success: true, data }; } } catch (e) { return Promise.resolve({ success: false, message: "Failed to login" }); diff --git a/frontend/src/Components/BackButton.tsx b/frontend/src/Components/BackButton.tsx deleted file mode 100644 index 7a1ac81..0000000 --- a/frontend/src/Components/BackButton.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100644 index 5f402c0..0000000 --- a/frontend/src/Components/BackgroundAnimation.tsx +++ /dev/null @@ -1,24 +0,0 @@ -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/InputField.tsx b/frontend/src/Components/InputField.tsx deleted file mode 100644 index 639b4ca..0000000 --- a/frontend/src/Components/InputField.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * 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 deleted file mode 100644 index 3658cbf..0000000 --- a/frontend/src/Components/LoginCheck.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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>; -}): number { - const user: NewUser = { - username: props.username, - password: props.password, - }; - api - .login(user) - .then((response: APIResponse) => { - if (response.success) { - if (response.data !== undefined) { - const token = response.data; - //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"); - } - }) - .catch((error) => { - console.error("An error occurred during login:", error); - }); - - return 0; -} - -export default LoginCheck; diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx deleted file mode 100644 index d7768c0..0000000 --- a/frontend/src/Components/LoginField.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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} - /> -