diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 320327a..7f4a89c 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -29,6 +29,7 @@ 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) } // This struct is a wrapper type that holds the database connection @@ -256,6 +257,31 @@ 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 + 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 +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 9124c45..f791066 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -371,3 +371,42 @@ 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 +} 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..366d932 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, + 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..57a1969 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,7 @@ 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 @@ -51,50 +48,6 @@ 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}) } @@ -103,181 +56,3 @@ 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 new file mode 100644 index 0000000..6a430e9 --- /dev/null +++ b/backend/internal/handlers/handlers_project_related.go @@ -0,0 +1,98 @@ +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 new file mode 100644 index 0000000..8754afd --- /dev/null +++ b/backend/internal/handlers/handlers_report_related.go @@ -0,0 +1,62 @@ +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 + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract project name and week from query parameters + projectName := c.Query("projectName") + week := c.Query("week") + + // 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 new file mode 100644 index 0000000..e90abd0 --- /dev/null +++ b/backend/internal/handlers/handlers_user_related.go @@ -0,0 +1,116 @@ +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 { + 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") +} + +// 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}) +} + +// 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 e0ea1ef..b704cc8 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -19,3 +19,26 @@ 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"` +}