From 13d3035e49a8cef39fba84d6db095855df83051a Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Fri, 29 Mar 2024 14:37:22 +0100 Subject: [PATCH] Major refactor, splitting user handlers into separate files and changes to how the database is accessed --- backend/internal/database/middleware.go | 17 ++ backend/internal/handlers/global_state.go | 44 --- .../internal/handlers/global_state_test.go | 15 - .../handlers/handlers_user_related.go | 269 ------------------ .../handlers_project_related.go | 57 ++-- .../{ => reports}/handlers_report_related.go | 31 +- .../internal/handlers/users/ChangeUserName.go | 44 +++ .../handlers/users/GetUsersProjects.go | 22 ++ .../internal/handlers/users/ListAllUsers.go | 31 ++ backend/internal/handlers/users/Login.go | 65 +++++ backend/internal/handlers/users/LoginRenew.go | 44 +++ .../internal/handlers/users/PromoteToAdmin.go | 42 +++ backend/internal/handlers/users/Register.go | 38 +++ backend/internal/handlers/users/UserDelete.go | 43 +++ backend/main.go | 77 +++-- 15 files changed, 439 insertions(+), 400 deletions(-) create mode 100644 backend/internal/database/middleware.go delete mode 100644 backend/internal/handlers/global_state.go delete mode 100644 backend/internal/handlers/global_state_test.go delete mode 100644 backend/internal/handlers/handlers_user_related.go rename backend/internal/handlers/{ => projects}/handlers_project_related.go (83%) rename backend/internal/handlers/{ => reports}/handlers_report_related.go (83%) create mode 100644 backend/internal/handlers/users/ChangeUserName.go create mode 100644 backend/internal/handlers/users/GetUsersProjects.go create mode 100644 backend/internal/handlers/users/ListAllUsers.go create mode 100644 backend/internal/handlers/users/Login.go create mode 100644 backend/internal/handlers/users/LoginRenew.go create mode 100644 backend/internal/handlers/users/PromoteToAdmin.go create mode 100644 backend/internal/handlers/users/Register.go create mode 100644 backend/internal/handlers/users/UserDelete.go diff --git a/backend/internal/database/middleware.go b/backend/internal/database/middleware.go new file mode 100644 index 0000000..69fa3a2 --- /dev/null +++ b/backend/internal/database/middleware.go @@ -0,0 +1,17 @@ +package database + +import "github.com/gofiber/fiber/v2" + +// Simple middleware that provides a shared database pool as a local key "db" +func DbMiddleware(db *Database) func(c *fiber.Ctx) error { + return func(c *fiber.Ctx) error { + c.Locals("db", db) + return c.Next() + } +} + +// Helper function to get the database from the context, without fiddling with casts +func GetDb(c *fiber.Ctx) Database { + // Dereference a pointer to a local, casted to a pointer to a Database + return *c.Locals("db").(*Database) +} diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go deleted file mode 100644 index 0db4340..0000000 --- a/backend/internal/handlers/global_state.go +++ /dev/null @@ -1,44 +0,0 @@ -package handlers - -import ( - "ttime/internal/database" - - "github.com/gofiber/fiber/v2" -) - -// The actual interface that we will use -type GlobalState interface { - Register(c *fiber.Ctx) error // To register a new user - UserDelete(c *fiber.Ctx) error // To delete a user - Login(c *fiber.Ctx) error // To get the token - LoginRenew(c *fiber.Ctx) error // To renew the token - CreateProject(c *fiber.Ctx) error // To create a new project - GetUserProjects(c *fiber.Ctx) error // To get all projects - SubmitWeeklyReport(c *fiber.Ctx) error - GetWeeklyReport(c *fiber.Ctx) error - SignReport(c *fiber.Ctx) error - GetProject(c *fiber.Ctx) error - AddUserToProjectHandler(c *fiber.Ctx) error - PromoteToAdmin(c *fiber.Ctx) error - GetWeeklyReportsUserHandler(c *fiber.Ctx) error - IsProjectManagerHandler(c *fiber.Ctx) error - DeleteProject(c *fiber.Ctx) error // To delete a project // WIP - ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database - ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project - ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project - ChangeUserName(c *fiber.Ctx) error // WIP - GetAllUsersProject(c *fiber.Ctx) error // WIP - GetUnsignedReports(c *fiber.Ctx) error // - UpdateWeeklyReport(c *fiber.Ctx) error - RemoveProject(c *fiber.Ctx) error -} - -// "Constructor" -func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db} -} - -// The global state, which implements all the handlers -type GState struct { - Db database.Database -} diff --git a/backend/internal/handlers/global_state_test.go b/backend/internal/handlers/global_state_test.go deleted file mode 100644 index c0b64f7..0000000 --- a/backend/internal/handlers/global_state_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package handlers - -import ( - "testing" - "ttime/internal/database" -) - -// The actual interface that we will use -func TestGlobalState(t *testing.T) { - db := database.DbConnect(":memory:") - gs := NewGlobalState(db) - if gs == nil { - t.Error("NewGlobalState returned nil") - } -} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go deleted file mode 100644 index bc4ae2d..0000000 --- a/backend/internal/handlers/handlers_user_related.go +++ /dev/null @@ -1,269 +0,0 @@ -package handlers - -import ( - "time" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2/log" - - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" -) - -// Register is a simple handler that registers a new user -// -// @Summary Register -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "User to register" -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /register [post] -func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Adding user:", u.Username) - if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - log.Warn("Error adding user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User added:", u.Username) - return c.Status(200).SendString("User added") -} - -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -// -// @Summary UserDelete -// @Description UserDelete deletes a user from the database -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {string} string "User deleted" -// @Failure 403 {string} string "You can only delete yourself" -// @Failure 500 {string} string "Internal server error" -// @Failure 401 {string} string "Unauthorized" -// @Router /userdelete/{username} [delete] -func (gs *GState) UserDelete(c *fiber.Ctx) error { - // Read from path parameters - username := c.Params("username") - - // Read username from Locals - auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) - - if username == auth_username { - log.Info("User tried to delete itself") - return c.Status(403).SendString("You can't delete yourself") - } - - if err := gs.Db.RemoveUser(username); err != nil { - log.Warn("Error deleting user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User deleted:", username) - return c.Status(200).SendString("User deleted") -} - -// Login is a simple login handler that returns a JWT token -// -// @Summary login -// @Description logs the user in and returns a jwt token -// @Tags User -// @Accept json -// @Param NewUser body types.NewUser true "login info" -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] -func (gs *GState) Login(c *fiber.Ctx) error { - // The body type is identical to a NewUser - - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Username logging in:", u.Username) - if !gs.Db.CheckUser(u.Username, u.Password) { - log.Info("User not found") - return c.SendStatus(fiber.StatusUnauthorized) - } - - isAdmin, err := gs.Db.IsSiteAdmin(u.Username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - // Create the Claims - claims := jwt.MapClaims{ - "name": u.Username, - "admin": isAdmin, - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - log.Info("Token created for user:", u.Username) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - println("Successfully signed token for user:", u.Username) - return c.JSON(types.Token{Token: t}) -} - -// LoginRenew is a simple handler that renews the token -// -// @Summary LoginRenews -// @Description renews the users token -// @Security bererToken -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginerenew [post] -func (gs *GState) LoginRenew(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - - claims := user.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - renewed := jwt.MapClaims{ - "name": claims["name"], - "admin": claims["admin"], - "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) - return c.JSON(types.Token{Token: t}) -} - -// ListAllUsers is a handler that returns a list of all users in the application database -// -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {json} json "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] -func (gs *GState) ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := gs.Db.GetAllUsersApplication() - if err != nil { - log.Info("Error getting users from db:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning all users") - // Return the list of users as JSON - return c.JSON(users) -} - -func (gs *GState) GetAllUsersProject(c *fiber.Ctx) error { - // Get all users from a project - projectName := c.Params("projectName") - users, err := gs.Db.GetAllUsersProject(projectName) - if err != nil { - log.Info("Error getting users from project:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning all users") - // Return the list of users as JSON - return c.JSON(users) -} - -// @Summary PromoteToAdmin -// @Description promote chosen user to admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] -func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { - // Extract the username from the request body - var newUser types.NewUser - if err := c.BodyParser(&newUser); err != nil { - return c.Status(400).SendString("Bad request") - } - username := newUser.Username - - log.Info("Promoting user to admin:", username) // Debug print - - // Promote the user to a site admin in the database - if err := gs.Db.PromoteToAdmin(username); err != nil { - log.Info("Error promoting user to admin:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("User promoted to admin successfully:", username) // Debug print - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} - -// ChangeUserName changes a user's username in the database -func (gs *GState) ChangeUserName(c *fiber.Ctx) error { - // Check token and get username of current user - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - adminUsername := claims["name"].(string) - log.Info(adminUsername) - - // Extract the necessary parameters from the request - data := new(types.StrNameChange) - if err := c.BodyParser(data); err != nil { - log.Info("Error parsing username") - return c.Status(400).SendString(err.Error()) - } - - // Check if the current user is an admin - isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) - if err != nil { - log.Warn("Error checking if admin:", err) - return c.Status(500).SendString(err.Error()) - } else if !isAdmin { - log.Warn("Tried changing name when not admin") - return c.Status(401).SendString("You cannot change name unless you are an admin") - } - - // Change the user's name in the database - if err := gs.Db.ChangeUserName(data.PrevName, data.NewName); err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/projects/handlers_project_related.go similarity index 83% rename from backend/internal/handlers/handlers_project_related.go rename to backend/internal/handlers/projects/handlers_project_related.go index d63d7eb..3429504 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/projects/handlers_project_related.go @@ -1,7 +1,8 @@ -package handlers +package projects import ( "strconv" + db "ttime/internal/database" "ttime/internal/types" "github.com/gofiber/fiber/v2" @@ -10,7 +11,7 @@ import ( ) // CreateProject is a simple handler that creates a new project -func (gs *GState) CreateProject(c *fiber.Ctx) error { +func CreateProject(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) p := new(types.NewProject) @@ -23,19 +24,19 @@ func (gs *GState) CreateProject(c *fiber.Ctx) error { claims := user.Claims.(jwt.MapClaims) owner := claims["name"].(string) - if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { + if err := db.GetDb(c).AddProject(p.Name, p.Description, owner); err != nil { return c.Status(500).SendString(err.Error()) } return c.Status(200).SendString("Project added") } -func (gs *GState) DeleteProject(c *fiber.Ctx) error { +func DeleteProject(c *fiber.Ctx) error { projectID := c.Params("projectID") username := c.Params("username") - if err := gs.Db.DeleteProject(projectID, username); err != nil { + if err := db.GetDb(c).DeleteProject(projectID, username); err != nil { return c.Status(500).SendString((err.Error())) } @@ -43,14 +44,14 @@ func (gs *GState) DeleteProject(c *fiber.Ctx) error { } // GetUserProjects returns all projects that the user is a member of -func (gs *GState) GetUserProjects(c *fiber.Ctx) error { +func 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) + projects, err := db.GetDb(c).GetProjectsForUser(username) if err != nil { return c.Status(500).SendString(err.Error()) } @@ -60,7 +61,7 @@ func (gs *GState) GetUserProjects(c *fiber.Ctx) error { } // ProjectRoleChange is a handler that changes a user's role within a project -func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { +func ProjectRoleChange(c *fiber.Ctx) error { //check token and get username of current user user := c.Locals("user").(*jwt.Token) @@ -77,7 +78,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) // Dubble diping and checcking if current user is - if ismanager, err := gs.Db.IsProjectManager(username, data.Projectname); err != nil { + if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil { log.Warn("Error checking if projectmanager:", err) return c.Status(500).SendString(err.Error()) } else if !ismanager { @@ -86,7 +87,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { } // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, data.Projectname, data.Role); err != nil { + if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { return c.Status(500).SendString(err.Error()) } @@ -95,7 +96,7 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { } // GetProject retrieves a specific project by its ID -func (gs *GState) GetProject(c *fiber.Ctx) error { +func GetProject(c *fiber.Ctx) error { // Extract the project ID from the request parameters or body projectID := c.Params("projectID") if projectID == "" { @@ -112,7 +113,7 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { } // Get the project from the database by its ID - project, err := gs.Db.GetProject(projectIDInt) + project, err := db.GetDb(c).GetProject(projectIDInt) if err != nil { log.Info("Error getting project:", err) return c.Status(500).SendString(err.Error()) @@ -123,7 +124,7 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { return c.JSON(project) } -func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { +func ListAllUsersProject(c *fiber.Ctx) error { // Extract the project name from the request parameters or body projectName := c.Params("projectName") if projectName == "" { @@ -137,7 +138,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { username := claims["name"].(string) // Check if the user is a project manager for the specified project - isManager, err := gs.Db.IsProjectManager(username, projectName) + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) if err != nil { log.Info("Error checking project manager status:", err) return c.Status(500).SendString(err.Error()) @@ -145,7 +146,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { // If the user is not a project manager, check if the user is a site admin if !isManager { - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -157,7 +158,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { } // Get all users associated with the project from the database - users, err := gs.Db.GetAllUsersProject(projectName) + users, err := db.GetDb(c).GetAllUsersProject(projectName) if err != nil { log.Info("Error getting users for project:", err) return c.Status(500).SendString(err.Error()) @@ -170,7 +171,7 @@ func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { } // AddUserToProjectHandler is a handler that adds a user to a project with a specified role -func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { +func AddUserToProjectHandler(c *fiber.Ctx) error { // Extract necessary parameters from the request var requestData struct { Username string `json:"username"` @@ -188,7 +189,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { adminUsername := claims["name"].(string) log.Info("Admin username from claims:", adminUsername) - isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -200,7 +201,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { } // Add the user to the project with the specified role - err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + err = db.GetDb(c).AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) if err != nil { log.Info("Error adding user to project:", err) return c.Status(500).SendString(err.Error()) @@ -212,7 +213,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { } // IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project -func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { +func IsProjectManagerHandler(c *fiber.Ctx) error { // Get the username from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -224,7 +225,7 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { log.Info("Checking if user ", username, " is a project manager for project ", projectName) // Check if the user is a project manager for the specified project - isManager, err := gs.Db.IsProjectManager(username, projectName) + isManager, err := db.GetDb(c).IsProjectManager(username, projectName) if err != nil { log.Info("Error checking project manager status:", err) return c.Status(500).SendString(err.Error()) @@ -234,7 +235,7 @@ func (gs *GState) IsProjectManagerHandler(c *fiber.Ctx) error { return c.JSON(fiber.Map{"isProjectManager": isManager}) } -func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { +func GetProjectTimesHandler(c *fiber.Ctx) error { // Get the username from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -248,7 +249,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { } // Get all users in the project and roles - userProjects, err := gs.Db.GetAllUsersProject(projectName) + userProjects, err := db.GetDb(c).GetAllUsersProject(projectName) if err != nil { log.Info("Error getting users in project:", err) return c.Status(500).SendString(err.Error()) @@ -265,7 +266,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { // If the user is admin if !isMember { - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -277,7 +278,7 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { } // Get project times - projectTimes, err := gs.Db.GetProjectTimes(projectName) + projectTimes, err := db.GetDb(c).GetProjectTimes(projectName) if err != nil { log.Info("Error getting project times:", err) return c.Status(500).SendString(err.Error()) @@ -288,13 +289,13 @@ func (gs *GState) GetProjectTimesHandler(c *fiber.Ctx) error { return c.JSON(projectTimes) } -func (gs *GState) RemoveProject(c *fiber.Ctx) error { +func RemoveProject(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) username := claims["name"].(string) // Check if the user is a site admin - isAdmin, err := gs.Db.IsSiteAdmin(username) + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) if err != nil { log.Info("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) @@ -307,7 +308,7 @@ func (gs *GState) RemoveProject(c *fiber.Ctx) error { projectName := c.Params("projectName") - if err := gs.Db.RemoveProject(projectName); err != nil { + if err := db.GetDb(c).RemoveProject(projectName); err != nil { return c.Status(500).SendString((err.Error())) } diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/reports/handlers_report_related.go similarity index 83% rename from backend/internal/handlers/handlers_report_related.go rename to backend/internal/handlers/reports/handlers_report_related.go index 52e1564..1c84d52 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/reports/handlers_report_related.go @@ -1,7 +1,8 @@ -package handlers +package reports import ( "strconv" + db "ttime/internal/database" "ttime/internal/types" "github.com/gofiber/fiber/v2" @@ -9,7 +10,7 @@ import ( "github.com/golang-jwt/jwt/v5" ) -func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { +func SubmitWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -31,7 +32,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { 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 { + if err := db.GetDb(c).AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { log.Info("Error adding weekly report to db:", err) return c.Status(500).SendString(err.Error()) } @@ -41,7 +42,7 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { } // Handler for retrieving weekly report -func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { +func GetWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the request user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -66,7 +67,7 @@ func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { } // Call the database function to get the weekly report - report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) + report, err := db.GetDb(c).GetWeeklyReport(username, projectName, weekInt) if err != nil { log.Info("Error getting weekly report from db:", err) return c.Status(500).SendString(err.Error()) @@ -81,7 +82,7 @@ type ReportId struct { ReportId int } -func (gs *GState) SignReport(c *fiber.Ctx) error { +func SignReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -98,7 +99,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { log.Info("Signing report for: ", rid.ReportId) // Get the project manager's ID - projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) + projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) if err != nil { log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") @@ -106,7 +107,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { log.Info("Project manager ID: ", projectManagerID) // Call the database function to sign the weekly report - err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) + err = db.GetDb(c).SignWeeklyReport(rid.ReportId, projectManagerID) if err != nil { log.Info("Error signing weekly report:", err) return c.Status(500).SendString(err.Error()) @@ -115,7 +116,7 @@ func (gs *GState) SignReport(c *fiber.Ctx) error { return c.Status(200).SendString("Weekly report signed successfully") } -func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { +func GetUnsignedReports(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -132,7 +133,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { } // Get the project manager's ID - isProjectManager, err := gs.Db.IsProjectManager(projectManagerUsername, projectName) + isProjectManager, err := db.GetDb(c).IsProjectManager(projectManagerUsername, projectName) if err != nil { log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") @@ -140,7 +141,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { log.Info("User is Project Manager: ", isProjectManager) // Call the database function to get the unsigned weekly reports - reports, err := gs.Db.GetUnsignedWeeklyReports(projectName) + reports, err := db.GetDb(c).GetUnsignedWeeklyReports(projectName) if err != nil { log.Info("Error getting unsigned weekly reports:", err) return c.Status(500).SendString(err.Error()) @@ -152,7 +153,7 @@ func (gs *GState) GetUnsignedReports(c *fiber.Ctx) error { } // GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project -func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { +func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -166,7 +167,7 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { // the returned list of reports will (should) allways be empty. // Retrieve weekly reports for the user in the project from the database - reports, err := gs.Db.GetWeeklyReportsUser(username, projectName) + reports, err := db.GetDb(c).GetWeeklyReportsUser(username, projectName) if err != nil { log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) return c.Status(500).SendString(err.Error()) @@ -178,7 +179,7 @@ func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { return c.JSON(reports) } -func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error { +func UpdateWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) @@ -203,7 +204,7 @@ func (gs *GState) UpdateWeeklyReport(c *fiber.Ctx) error { } // Update the weekly report in the database - if err := gs.Db.UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { + if err := db.GetDb(c).UpdateWeeklyReport(updateReport.ProjectName, username, updateReport.Week, updateReport.DevelopmentTime, updateReport.MeetingTime, updateReport.AdminTime, updateReport.OwnWorkTime, updateReport.StudyTime, updateReport.TestingTime); err != nil { log.Info("Error updating weekly report in db:", err) return c.Status(500).SendString(err.Error()) } diff --git a/backend/internal/handlers/users/ChangeUserName.go b/backend/internal/handlers/users/ChangeUserName.go new file mode 100644 index 0000000..75032e4 --- /dev/null +++ b/backend/internal/handlers/users/ChangeUserName.go @@ -0,0 +1,44 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// ChangeUserName changes a user's username in the database +func ChangeUserName(c *fiber.Ctx) error { + // Check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info(adminUsername) + + // Extract the necessary parameters from the request + data := new(types.StrNameChange) + if err := c.BodyParser(data); err != nil { + log.Info("Error parsing username") + return c.Status(400).SendString(err.Error()) + } + + // Check if the current user is an admin + isAdmin, err := db.GetDb(c).IsSiteAdmin(adminUsername) + if err != nil { + log.Warn("Error checking if admin:", err) + return c.Status(500).SendString(err.Error()) + } else if !isAdmin { + log.Warn("Tried changing name when not admin") + return c.Status(401).SendString("You cannot change name unless you are an admin") + } + + // Change the user's name in the database + if err := db.GetDb(c).ChangeUserName(data.PrevName, data.NewName); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/users/GetUsersProjects.go b/backend/internal/handlers/users/GetUsersProjects.go new file mode 100644 index 0000000..10a6ec6 --- /dev/null +++ b/backend/internal/handlers/users/GetUsersProjects.go @@ -0,0 +1,22 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +func GetAllUsersProject(c *fiber.Ctx) error { + // Get all users from a project + projectName := c.Params("projectName") + users, err := db.GetDb(c).GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users from project:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning all users") + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/users/ListAllUsers.go b/backend/internal/handlers/users/ListAllUsers.go new file mode 100644 index 0000000..1cae76c --- /dev/null +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -0,0 +1,31 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// ListAllUsers is a handler that returns a list of all users in the application database +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {json} json "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] +func ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := db.GetDb(c).GetAllUsersApplication() + if err != nil { + log.Info("Error getting users from db:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning all users") + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go new file mode 100644 index 0000000..c4d6c60 --- /dev/null +++ b/backend/internal/handlers/users/Login.go @@ -0,0 +1,65 @@ +package users + +import ( + "time" + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// Login is a simple login handler that returns a JWT token +// @Summary login +// @Description logs the user in and returns a jwt token +// @Tags User +// @Accept json +// @Param NewUser body types.NewUser true "login info" +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] +func Login(c *fiber.Ctx) error { + // The body type is identical to a NewUser + + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + log.Warn("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Username logging in:", u.Username) + if !db.GetDb(c).CheckUser(u.Username, u.Password) { + log.Info("User not found") + return c.SendStatus(fiber.StatusUnauthorized) + } + + isAdmin, err := db.GetDb(c).IsSiteAdmin(u.Username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + // Create the Claims + claims := jwt.MapClaims{ + "name": u.Username, + "admin": isAdmin, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + log.Info("Token created for user:", u.Username) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + log.Warn("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) + } + + println("Successfully signed token for user:", u.Username) + return c.JSON(types.Token{Token: t}) +} diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go new file mode 100644 index 0000000..78eadfd --- /dev/null +++ b/backend/internal/handlers/users/LoginRenew.go @@ -0,0 +1,44 @@ +package users + +import ( + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// LoginRenew is a simple handler that renews the token +// @Summary LoginRenews +// @Description renews the users token +// @Security bererToken +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginerenew [post] +func LoginRenew(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) + + claims := user.Claims.(jwt.MapClaims) + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + renewed := jwt.MapClaims{ + "name": claims["name"], + "admin": claims["admin"], + "exp": claims["exp"], + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + t, err := token.SignedString([]byte("secret")) + if err != nil { + log.Warn("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) + } + + log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) + return c.JSON(types.Token{Token: t}) +} diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go new file mode 100644 index 0000000..4a21758 --- /dev/null +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -0,0 +1,42 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// @Summary PromoteToAdmin +// @Description promote chosen user to admin +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] +func PromoteToAdmin(c *fiber.Ctx) error { + // Extract the username from the request body + var newUser types.NewUser + if err := c.BodyParser(&newUser); err != nil { + return c.Status(400).SendString("Bad request") + } + username := newUser.Username + + log.Info("Promoting user to admin:", username) // Debug print + + // Promote the user to a site admin in the database + if err := db.GetDb(c).PromoteToAdmin(username); err != nil { + log.Info("Error promoting user to admin:", err) // Debug print + return c.Status(500).SendString(err.Error()) + } + + log.Info("User promoted to admin successfully:", username) // Debug print + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go new file mode 100644 index 0000000..9977246 --- /dev/null +++ b/backend/internal/handlers/users/Register.go @@ -0,0 +1,38 @@ +package users + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// Register is a simple handler that registers a new user +// +// @Summary Register +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "User to register" +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /register [post] +func Register(c *fiber.Ctx) error { + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + log.Warn("Error parsing body") + return c.Status(400).SendString(err.Error()) + } + + log.Info("Adding user:", u.Username) + if err := db.GetDb(c).AddUser(u.Username, u.Password); err != nil { + log.Warn("Error adding user:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("User added:", u.Username) + return c.Status(200).SendString("User added") +} diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go new file mode 100644 index 0000000..5957c2d --- /dev/null +++ b/backend/internal/handlers/users/UserDelete.go @@ -0,0 +1,43 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +// +// @Summary UserDelete +// @Description UserDelete deletes a user from the database +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {string} string "User deleted" +// @Failure 403 {string} string "You can only delete yourself" +// @Failure 500 {string} string "Internal server error" +// @Failure 401 {string} string "Unauthorized" +// @Router /userdelete/{username} [delete] +func UserDelete(c *fiber.Ctx) error { + // Read from path parameters + username := c.Params("username") + + // Read username from Locals + auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) + + if username == auth_username { + log.Info("User tried to delete itself") + return c.Status(403).SendString("You can't delete yourself") + } + + if err := db.GetDb(c).RemoveUser(username); err != nil { + log.Warn("Error deleting user:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("User deleted:", username) + return c.Status(200).SendString("User deleted") +} diff --git a/backend/main.go b/backend/main.go index 669bbc7..ebe5660 100644 --- a/backend/main.go +++ b/backend/main.go @@ -6,7 +6,9 @@ import ( _ "ttime/docs" "ttime/internal/config" "ttime/internal/database" - "ttime/internal/handlers" + "ttime/internal/handlers/projects" + "ttime/internal/handlers/reports" + "ttime/internal/handlers/users" "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" @@ -54,24 +56,28 @@ func main() { // Connect to the database db := database.DbConnect(conf.DbPath) + // Migrate the database if err = db.Migrate(); err != nil { fmt.Println("Error migrating database: ", err) os.Exit(1) } + // Migrate sample data, should not be used in production if err = db.MigrateSampleData(); err != nil { fmt.Println("Error migrating sample data: ", err) os.Exit(1) } - // Get our global state - gs := handlers.NewGlobalState(db) // Create the server server := fiber.New() + // We want some logs server.Use(logger.New()) + // Sets up db middleware, accessed as Local "db" key + server.Use(database.DbMiddleware(&db)) + // Mounts the swagger documentation, this is available at /swagger/index.html server.Get("/swagger/*", swagger.HandlerDefault) @@ -79,37 +85,50 @@ func main() { // This will likely be replaced by an embedded filesystem in the future server.Static("/", "./static") - // Register our unprotected routes - server.Post("/api/register", gs.Register) - server.Post("/api/login", gs.Login) + // Create a group for our API + api := server.Group("/api") - // Every route from here on will require a valid JWT + // Register our unprotected routes + api.Post("/register", users.Register) + api.Post("/login", users.Login) + + // Every route from here on will require a valid + // JWT bearer token authentication in the header server.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) - // Protected routes (require a valid JWT bearer token authentication header) - server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport) - server.Get("/api/getUserProjects", gs.GetUserProjects) - server.Post("/api/loginrenew", gs.LoginRenew) - server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches - server.Delete("api/project/:projectID", gs.DeleteProject) // WIP - server.Post("/api/project", gs.CreateProject) // WIP - server.Get("/api/project/:projectId", gs.GetProject) - server.Get("/api/project/getAllUsers", gs.GetAllUsersProject) - server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) - server.Get("/api/getUnsignedReports/:projectName", gs.GetUnsignedReports) - server.Post("/api/signReport", gs.SignReport) - server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) - server.Put("/api/changeUserName", gs.ChangeUserName) - server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) - server.Get("/api/users/all", gs.ListAllUsers) - server.Get("/api/getWeeklyReportsUser/:projectName", gs.GetWeeklyReportsUserHandler) - server.Get("/api/checkIfProjectManager/:projectName", gs.IsProjectManagerHandler) - server.Post("/api/ProjectRoleChange", gs.ProjectRoleChange) - server.Get("/api/getUsersProject/:projectName", gs.ListAllUsersProject) - server.Put("/api/updateWeeklyReport", gs.UpdateWeeklyReport) - server.Delete("/api/removeProject/:projectName", gs.RemoveProject) + // All user related routes + // userGroup := api.Group("/user") // Not currently in use + api.Post("/login", users.Login) + api.Post("/register", users.Register) + api.Post("/loginrenew", users.LoginRenew) + api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches + api.Put("/changeUserName", users.ChangeUserName) + api.Get("/users/all", users.ListAllUsers) + api.Post("/promoteToAdmin", users.PromoteToAdmin) + api.Get("/project/getAllUsers", users.GetAllUsersProject) + + // All project related routes + // projectGroup := api.Group("/project") // Not currently in use + api.Get("/getUserProjects", projects.GetUserProjects) + api.Delete("/project/:projectID", projects.DeleteProject) + api.Post("/project", projects.CreateProject) + api.Get("/project/:projectId", projects.GetProject) + api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) + api.Post("/ProjectRoleChange", projects.ProjectRoleChange) + api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) + api.Delete("/removeProject/:projectName", projects.RemoveProject) + + // All report related routes + // reportGroup := api.Group("/report") // Not currently in use + api.Get("/getWeeklyReport", reports.GetWeeklyReport) + api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) + api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) + api.Post("/signReport", reports.SignReport) + api.Put("/addUserToProject", projects.AddUserToProjectHandler) + api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) + api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port))