diff --git a/.gitignore b/.gitignore index bdbfff8..313b735 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ dist/ .vscode/ .idea/ .DS_Store +.go.work.sum # Ignore configuration files .env diff --git a/backend/Makefile b/backend/Makefile index da0e254..65a2f3c 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -10,6 +10,7 @@ DB_FILE = db.sqlite3 # Directory containing migration SQL scripts MIGRATIONS_DIR = internal/database/migrations +SAMPLE_DATA_DIR = internal/database/sample_data # Build target build: @@ -54,6 +55,14 @@ migrate: sqlite3 $(DB_FILE) < $$file; \ done +sampledata: + @echo "If this ever fails, run make clean and try again" + @echo "Migrating database $(DB_FILE) using SQL scripts in $(SAMPLE_DATA_DIR)" + @for file in $(wildcard $(SAMPLE_DATA_DIR)/*.sql); do \ + echo "Applying migration: $$file"; \ + sqlite3 $(DB_FILE) < $$file; \ + done + # Target added primarily for CI/CD to ensure that the database is created before running tests db.sqlite3: make migrate diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5cbb13f..25dd04b 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -20,6 +20,7 @@ type Database interface { GetUserId(username string) (int, error) AddProject(name string, description string, username string) error Migrate() error + MigrateSampleData() 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 @@ -32,6 +33,7 @@ type Database interface { GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error + IsSiteAdmin(username string) (bool, error) } // This struct is a wrapper type that holds the database connection @@ -48,6 +50,9 @@ type UserProjectMember struct { //go:embed migrations var scripts embed.FS +//go:embed sample_data +var sampleData embed.FS + // TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" @@ -59,9 +64,10 @@ const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" -const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id - FROM projects JOIN user_roles ON projects.id = user_roles.project_id - JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;` +const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p + JOIN user_roles ur ON p.id = ur.project_id + JOIN users u ON ur.user_id = u.id + WHERE u.username = ?` // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -106,7 +112,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) { // GetProject retrieves a specific project by its ID. func (d *Db) GetProject(projectId int) (types.Project, error) { var project types.Project - err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") + err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId) + if err != nil { + println("Error getting project: ", err) + } return project, err } @@ -313,6 +322,26 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { return err } +// IsSiteAdmin checks if a given username is a site admin +func (d *Db) IsSiteAdmin(username string) (bool, error) { + // Define the SQL query to check if the user is a site admin + query := ` + SELECT COUNT(*) FROM site_admin + JOIN users ON site_admin.admin_id = users.id + WHERE users.username = ? + ` + + // Execute the query + var count int + err := d.Get(&count, query, username) + if err != nil { + return false, err + } + + // If count is greater than 0, the user is a site admin + return count > 0, nil +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { @@ -354,3 +383,42 @@ func (d *Db) Migrate() error { return nil } + +// MigrateSampleData applies sample data to the database. +func (d *Db) MigrateSampleData() error { + // Insert sample data + files, err := sampleData.ReadDir("sample_data") + if err != nil { + return err + } + + if len(files) == 0 { + println("No sample data files found") + } + tr := d.MustBegin() + + // Iterate over each SQL file and execute it + for _, file := range files { + if file.IsDir() || filepath.Ext(file.Name()) != ".sql" { + continue + } + + // This is perhaps not the most elegant way to do this + sqlBytes, err := sampleData.ReadFile("sample_data/" + file.Name()) + if err != nil { + return err + } + + sqlQuery := string(sqlBytes) + _, err = tr.Exec(sqlQuery) + if err != nil { + return err + } + } + + if tr.Commit() != nil { + return err + } + + return nil +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 09de45b..a7f3878 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -536,3 +536,33 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") } } + +func TestGetProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Add a user + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + // Add a project + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + // Retrieve the added project + project, err := db.GetProject(1) + if err != nil { + t.Error("GetProject failed:", err) + } + + // Check if the retrieved project matches the expected values + if project.Name != "testproject" { + t.Errorf("Expected Name to be testproject, got %s", project.Name) + } +} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index d2e2dd1..15b1373 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -4,11 +4,9 @@ -- password is the hashed password CREATE TABLE IF NOT EXISTS users ( 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 ); -- Users are commonly searched by username and userId CREATE INDEX IF NOT EXISTS users_username_index ON users (username); -CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId); \ No newline at end of file diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql new file mode 100644 index 0000000..4dac91b --- /dev/null +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -0,0 +1,35 @@ +INSERT OR IGNORE INTO users(username, password) +VALUES ("admin", "123"); + +INSERT OR IGNORE INTO users(username, password) +VALUES ("user", "123"); + +INSERT OR IGNORE INTO users(username, password) +VALUES ("user2", "123"); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest","test project", 1); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest2","test project2", 1); + +INSERT OR IGNORE INTO projects(name,description,owner_user_id) +VALUES ("projecttest3","test project3", 1); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (1,1,"project_manager"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (2,1,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,1,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,2,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (3,3,"member"); + +INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) +VALUES (2,1,"project_manager"); diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index c8beb1c..932451d 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -17,6 +17,9 @@ type GlobalState interface { SubmitWeeklyReport(c *fiber.Ctx) error GetWeeklyReport(c *fiber.Ctx) error SignReport(c *fiber.Ctx) error + GetProject(c *fiber.Ctx) error + AddUserToProjectHandler(c *fiber.Ctx) error + PromoteToAdmin(c *fiber.Ctx) error // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project @@ -31,29 +34,17 @@ type GlobalState interface { // UpdateCollection(c *fiber.Ctx) error // To update a collection // DeleteCollection(c *fiber.Ctx) error // To delete a collection // SignCollection(c *fiber.Ctx) error // To sign a collection - GetButtonCount(c *fiber.Ctx) error // For demonstration purposes - IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes - ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database - ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project - ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project + ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database + ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project + ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project } // "Constructor" func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db, ButtonCount: 0} + return &GState{Db: db} } // The global state, which implements all the handlers type GState struct { - Db database.Database - ButtonCount int -} - -func (gs *GState) GetButtonCount(c *fiber.Ctx) error { - return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) -} - -func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { - gs.ButtonCount++ - return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) + Db database.Database } diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index 6a430e9..f3a7ea0 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -5,6 +5,7 @@ import ( "ttime/internal/types" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" "github.com/golang-jwt/jwt/v5" ) @@ -66,33 +67,90 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { func (gs *GState) GetProject(c *fiber.Ctx) error { // Extract the project ID from the request parameters or body projectID := c.Params("projectID") + if projectID == "" { + log.Info("No project ID provided") + return c.Status(400).SendString("No project ID provided") + } + log.Info("Getting project with ID: ", projectID) // Parse the project ID into an integer projectIDInt, err := strconv.Atoi(projectID) if err != nil { + log.Info("Invalid project ID") return c.Status(400).SendString("Invalid project ID") } // Get the project from the database by its ID project, err := gs.Db.GetProject(projectIDInt) if err != nil { + log.Info("Error getting project:", err) return c.Status(500).SendString(err.Error()) } // Return the project as JSON + log.Info("Returning project: ", project.Name) return c.JSON(project) } func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { // Extract the project name from the request parameters or body projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } // Get all users associated with the project from the database users, err := gs.Db.GetAllUsersProject(projectName) if err != nil { + log.Info("Error getting users for project:", err) return c.Status(500).SendString(err.Error()) } + log.Info("Returning users for project: ", projectName) + // Return the list of users as JSON return c.JSON(users) } + +// AddUserToProjectHandler is a handler that adds a user to a project with a specified role +func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { + // Extract necessary parameters from the request + var requestData struct { + Username string `json:"username"` + ProjectName string `json:"projectName"` + Role string `json:"role"` + } + if err := c.BodyParser(&requestData); err != nil { + log.Info("Error parsing request body:", err) + return c.Status(400).SendString("Bad request") + } + + // Check if the user adding another user to the project is a site admin + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info("Admin username from claims:", adminUsername) + + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + log.Info("User is not a site admin:", adminUsername) + return c.Status(403).SendString("User is not a site admin") + } + + // Add the user to the project with the specified role + err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + if err != nil { + log.Info("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 509bd67..85eb6e2 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -5,6 +5,7 @@ import ( "ttime/internal/types" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" "github.com/golang-jwt/jwt/v5" ) @@ -16,78 +17,100 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { report := new(types.NewWeeklyReport) if err := c.BodyParser(report); err != nil { + log.Info("Error parsing weekly report") return c.Status(400).SendString(err.Error()) } // Make sure all the fields of the report are valid if report.Week < 1 || report.Week > 52 { + log.Info("Invalid week number") return c.Status(400).SendString("Invalid week number") } if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { + log.Info("Invalid time report") return c.Status(400).SendString("Invalid time report") } if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { + log.Info("Error adding weekly report") return c.Status(500).SendString(err.Error()) } + log.Info("Weekly report added") return c.Status(200).SendString("Time report added") } // Handler for retrieving weekly report func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the request - println("GetWeeklyReport") user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) username := claims["name"].(string) + log.Info("Getting weekly report for: ", username) + // Extract project name and week from query parameters projectName := c.Query("projectName") - println(projectName) week := c.Query("week") - println(week) + + if projectName == "" || week == "" { + log.Info("Missing project name or week number") + return c.Status(400).SendString("Missing project name or week number") + } // Convert week to integer weekInt, err := strconv.Atoi(week) if err != nil { + log.Info("Invalid week number") return c.Status(400).SendString("Invalid week number") } // Call the database function to get the weekly report report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) if err != nil { + log.Info("Error getting weekly report from db:", err) return c.Status(500).SendString(err.Error()) } + log.Info("Returning weekly report") // Return the retrieved weekly report return c.JSON(report) } +type ReportId struct { + ReportId int +} + func (gs *GState) SignReport(c *fiber.Ctx) error { // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) - managerUsername := claims["name"].(string) + projectManagerUsername := claims["name"].(string) - // Extract the report ID and project manager ID from request parameters - reportID, err := strconv.Atoi(c.Params("reportId")) - if err != nil { - return c.Status(400).SendString("Invalid report ID") + log.Info("Signing report for: ", projectManagerUsername) + + // Extract report ID from the request query parameters + // reportID := c.Query("reportId") + rid := new(ReportId) + if err := c.BodyParser(rid); err != nil { + return err } + log.Info("Signing report for: ", rid.ReportId) - // Call the database function to get the project manager ID - managerID, err := gs.Db.GetUserId(managerUsername) + // Get the project manager's ID + projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) if err != nil { + log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") } + log.Info("Project manager ID: ", projectManagerID) // Call the database function to sign the weekly report - err = gs.Db.SignWeeklyReport(reportID, managerID) + err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) if err != nil { - return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error()) + log.Info("Error signing weekly report:", err) + return c.Status(500).SendString(err.Error()) } - // Return success response return c.Status(200).SendString("Weekly report signed successfully") } diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 0619ea5..8f4108c 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -4,6 +4,8 @@ import ( "time" "ttime/internal/types" + "github.com/gofiber/fiber/v2/log" + "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" ) @@ -22,16 +24,17 @@ import ( func (gs *GState) Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { - println("Error parsing body") + log.Warn("Error parsing body") return c.Status(400).SendString(err.Error()) } - println("Adding user:", u.Username) + 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()) } - println("User added:", u.Username) + log.Info("User added:", u.Username) return c.Status(200).SendString("User added") } @@ -45,13 +48,16 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error { auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) if username != auth_username { + log.Info("User tried to delete another user") return c.Status(403).SendString("You can only delete yourself") } if err := gs.Db.RemoveUser(username); err != nil { + log.Warn("Error deleting user:", err) return c.Status(500).SendString(err.Error()) } + log.Info("User deleted:", username) return c.Status(200).SendString("User deleted") } @@ -60,13 +66,13 @@ 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") + log.Warn("Error parsing body") return c.Status(400).SendString(err.Error()) } - println("Username:", u.Username) + log.Info("Username logging in:", u.Username) if !gs.Db.CheckUser(u.Username, u.Password) { - println("User not found") + log.Info("User not found") return c.SendStatus(fiber.StatusUnauthorized) } @@ -79,23 +85,25 @@ func (gs *GState) Login(c *fiber.Ctx) error { // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - println("Token created for user:", u.Username) + 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 { - println("Error signing token") + log.Warn("Error signing token") return c.SendStatus(fiber.StatusInternalServerError) } - println("Successfully signed token for user:", u.Username) + log.Info("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) + + 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{ @@ -106,8 +114,11 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error { 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(fiber.Map{"token": t}) } @@ -116,9 +127,33 @@ 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) 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) +} diff --git a/backend/main.go b/backend/main.go index bc33942..9abe995 100644 --- a/backend/main.go +++ b/backend/main.go @@ -10,6 +10,7 @@ import ( "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/swagger" jwtware "github.com/gofiber/contrib/jwt" @@ -46,6 +47,12 @@ func main() { // Migrate the database if err = db.Migrate(); err != nil { fmt.Println("Error migrating database: ", err) + os.Exit(1) + } + + if err = db.MigrateSampleData(); err != nil { + fmt.Println("Error migrating sample data: ", err) + os.Exit(1) } // Get our global state @@ -53,6 +60,9 @@ func main() { // Create the server server := fiber.New() + server.Use(logger.New()) + + // Mounts the swagger documentation, this is available at /swagger/index.html server.Get("/swagger/*", swagger.HandlerDefault) // Mount our static files (Beware of the security implications of this!) @@ -61,11 +71,6 @@ func main() { // Register our unprotected routes server.Post("/api/register", gs.Register) - - // Register handlers for example button count - server.Get("/api/button", gs.GetButtonCount) - server.Post("/api/button", gs.IncrementButtonCount) - server.Post("/api/login", gs.Login) // Every route from here on will require a valid JWT @@ -73,12 +78,17 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) + // Protected routes (require a valid JWT bearer token authentication header) server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) + server.Get("/api/project/:projectId", gs.GetProject) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) + server.Post("/api/signReport", gs.SignReport) + server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) + server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 7a1ccd0..8fd66d3 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -29,11 +29,6 @@ interface API { project: NewProject, token: string, ): Promise>; - /** Gets all the projects of a user*/ - getUserProjects( - username: string, - token: string, - ): Promise>; /** Submit a weekly report */ submitWeeklyReport( project: NewWeeklyReport, @@ -46,6 +41,10 @@ interface API { week: string, token: string, ): Promise>; + /** Gets all the projects of a user*/ + getUserProjects(token: string): Promise>; + /** Gets a project from id*/ + getProject(id: number): Promise>; } // Export an instance of the API @@ -170,7 +169,7 @@ export const api: API = { } catch (e) { return Promise.resolve({ success: false, - message: "Failed to get user projects", + message: "API fucked", }); } }, @@ -253,4 +252,30 @@ export const api: API = { return Promise.resolve({ success: false, message: "Failed to login" }); } }, + + // Gets a projet by id, currently untested since we have no javascript-based tests + async getProject(id: number): Promise> { + try { + const response = await fetch(`/api/project/${id}`, { + method: "GET", + }); + + if (!response.ok) { + return { + success: false, + message: "Failed to get project: Response code " + response.status, + }; + } else { + const data = (await response.json()) as Project; + return { success: true, data }; + } + // The code below is garbage but satisfies the linter + // This needs fixing, do not copy this pattern + } catch (e: unknown) { + return { + success: false, + message: "Failed to get project: " + (e as Error).toString(), + }; + } + }, }; diff --git a/frontend/src/Components/AdminUserList.tsx b/frontend/src/Components/AdminUserList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx new file mode 100644 index 0000000..71d5e57 --- /dev/null +++ b/frontend/src/Components/ChangeUsername.tsx @@ -0,0 +1,38 @@ +import React, { useState } from "react"; +import { api } from "../API/API"; +import InputField from "./InputField"; +import BackButton from "./BackButton"; +import Button from "./Button"; + + +function ChangeUsername(): JSX.Element { + const [newUsername, setNewUsername] = useState(""); + + const handleChange = (e: React.ChangeEvent): void => { + setNewUsername(e.target.value); + }; + + const handleSubmit = async (): Promise => { + try { + // Call the API function to update the username + await api.updateUsername(newUsername); + // Optionally, add a success message or redirect the user + } catch (error) { + console.error("Error updating username:", error); + // Optionally, handle the error + } + }; + + return ( +
+ +
+ ); +} + +export default ChangeUsername; diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx new file mode 100644 index 0000000..db49724 --- /dev/null +++ b/frontend/src/Components/DeleteUser.tsx @@ -0,0 +1,34 @@ +import { User } from "../Types/goTypes"; +import { api, APIResponse } from "../API/API"; + +/** + * Use to remove a user from the system + * @param props - The username of user to remove + * @returns {boolean} True if removed, false if not + * @example + * const exampleUsername = "user"; + * DeleteUser({ usernameToDelete: exampleUsername }); + */ + +function DeleteUser(props: { usernameToDelete: string }): boolean { + //console.log(props.usernameToDelete); FOR DEBUG + let removed = false; + api + .removeUser( + props.usernameToDelete, + localStorage.getItem("accessToken") ?? "", + ) + .then((response: APIResponse) => { + if (response.success) { + removed = true; + } else { + console.error(response.message); + } + }) + .catch((error) => { + console.error("An error occurred during creation:", error); + }); + return removed; +} + +export default DeleteUser; diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 9321d73..b0e8771 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -50,8 +50,8 @@ export default function GetWeeklyReport(): JSX.Element { } }; - fetchWeeklyReport(); - }, []); + void fetchWeeklyReport(); + }, [projectName, token, username, week]); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index 819c5de..5cdb421 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import { Link } from "react-router-dom"; +import backgroundImage from "../assets/1.jpg"; function Header(): JSX.Element { const [isOpen, setIsOpen] = useState(false); @@ -11,7 +12,7 @@ function Header(): JSX.Element { return (
=> { const newWeeklyReport: NewWeeklyReport = { - projectName, + projectName: projectName ?? "", week, developmentTime, meetingTime, @@ -46,7 +45,7 @@ export default function NewWeeklyReport(): JSX.Element { } e.preventDefault(); void handleNewWeeklyReport(); - navigate("/project"); + navigate(-1); }} >
diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index facca39..7b003cb 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -48,7 +48,7 @@ export default function Register(): JSX.Element { { setUsername(e.target.value); }} @@ -56,7 +56,7 @@ export default function Register(): JSX.Element { { setPassword(e.target.value); }} diff --git a/frontend/src/Components/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx new file mode 100644 index 0000000..a22ef01 --- /dev/null +++ b/frontend/src/Components/UserInfoModal.tsx @@ -0,0 +1,54 @@ +import { Link } from "react-router-dom"; +import Button from "./Button"; +import DeleteUser from "./DeleteUser"; +import UserProjectListAdmin from "./UserProjectListAdmin"; + +function UserInfoModal(props: { + isVisible: boolean; + username: string; + onClose: () => void; +}): JSX.Element { + if (!props.isVisible) return <>; + + return ( +
+
+

{props.username}

+ +

+ (Change Username) +

+ +
+

+ Member of these projects: +

+
+ +
+
+
+
+
+
+ ); +} + +export default UserInfoModal; diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index b86076a..3d2bcae 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,5 +1,6 @@ -import { Link } from "react-router-dom"; +import { useState } from "react"; import { PublicUser } from "../Types/goTypes"; +import UserInfoModal from "./UserInfoModal"; /** * The props for the UserProps component @@ -9,27 +10,52 @@ interface UserProps { } /** - * A list of users for admin manage users page, that links admin to the right user page - * thanks to the state property - * @param props - The users to display + * A list of users for admin manage users page, that sets an onClick + * function for eact user
  • element, which displays a modul with + * user info. + * @param props - An array of users users to display * @returns {JSX.Element} The user list * @example - * const users = [{ id: 1, userName: "Random name" }]; + * const users = [{ id: 1, userName: "ExampleName" }]; * return ; */ export function UserListAdmin(props: UserProps): JSX.Element { + const [modalVisible, setModalVisible] = useState(false); + const [username, setUsername] = useState(""); + + const handleClick = (username: string): void => { + setUsername(username); + setModalVisible(true); + }; + + const handleClose = (): void => { + setUsername(""); + setModalVisible(false); + }; + return ( -
    -
      - {props.users.map((user) => ( - -
    • + <> + +
      +
        + {props.users.map((user) => ( +
      • { + handleClick(user.username); + }} + > {user.username}
      • - - ))} -
      -
      + ))} +
    +
    + ); } diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index 423e793..1b7b923 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -1,17 +1,17 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { api } from "../API/API"; import { Project } from "../Types/goTypes"; -const UserProjectListAdmin: React.FC = () => { +function UserProjectListAdmin(): JSX.Element { const [projects, setProjects] = useState([]); useEffect(() => { const fetchProjects = async (): Promise => { try { const token = localStorage.getItem("accessToken") ?? ""; - const username = getUsernameFromContext(); // Assuming you have a function to get the username from your context + // const username = props.username; - const response = await api.getUserProjects(username, token); + const response = await api.getUserProjects(token); if (response.success) { setProjects(response.data ?? []); } else { @@ -26,18 +26,16 @@ const UserProjectListAdmin: React.FC = () => { }, []); return ( -
    -

    User Projects

    +
      {projects.map((project) => (
    • {project.name} - {/* Add any additional project details you want to display */}
    • ))}
    ); -}; +} export default UserProjectListAdmin; diff --git a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx index 1756433..b130fae 100644 --- a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx +++ b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx @@ -1,8 +1,14 @@ +import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; +import ChangeUsername from "../../Components/ChangeUsername"; function AdminChangeUsername(): JSX.Element { - const content = <>; + const content = ( + <> + + + ); const buttons = ( <> @@ -13,13 +19,7 @@ function AdminChangeUsername(): JSX.Element { }} type="button" /> -
    + ); +} diff --git a/frontend/src/Pages/UserPages/UserProjectPage.tsx b/frontend/src/Pages/UserPages/UserProjectPage.tsx index 20fe6d7..80a0035 100644 --- a/frontend/src/Pages/UserPages/UserProjectPage.tsx +++ b/frontend/src/Pages/UserPages/UserProjectPage.tsx @@ -1,18 +1,20 @@ -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation, useParams } from "react-router-dom"; import BasicWindow from "../../Components/BasicWindow"; import BackButton from "../../Components/BackButton"; function UserProjectPage(): JSX.Element { + const { projectName } = useParams(); + const content = ( <>

    {useLocation().state}

    - +

    Your Time Reports

    - +

    New Time Report

    diff --git a/frontend/src/Pages/YourProjectsPage.tsx b/frontend/src/Pages/YourProjectsPage.tsx index aabc606..5de772f 100644 --- a/frontend/src/Pages/YourProjectsPage.tsx +++ b/frontend/src/Pages/YourProjectsPage.tsx @@ -1,16 +1,15 @@ -import React, { useState, createContext, useEffect } from "react"; +import { useState, createContext } from "react"; import { Project } from "../Types/goTypes"; -import { api } from "../API/API"; import { Link } from "react-router-dom"; import BasicWindow from "../Components/BasicWindow"; export const ProjectNameContext = createContext(""); function UserProjectPage(): JSX.Element { - const [projects, setProjects] = useState([]); - const [selectedProject, setSelectedProject] = useState(""); + /* const [projects, setProjects] = useState([]); + */ const [selectedProject, setSelectedProject] = useState(""); - const getProjects = async (): Promise => { + /* const getProjects = async (): Promise => { const username = localStorage.getItem("username") ?? ""; // replace with actual username const token = localStorage.getItem("accessToken") ?? ""; // replace with actual token const response = await api.getUserProjects(username, token); @@ -24,7 +23,30 @@ function UserProjectPage(): JSX.Element { // Call getProjects when the component mounts useEffect(() => { getProjects(); - }, []); + }, []); */ + + // Mock data + const projects: Project[] = [ + { + id: 1, + name: "Project Test App", + description: "Description 1", + owner: "Owner 1", + }, + { + id: 2, + name: "Project 2", + description: "Description 2", + owner: "Owner 2", + }, + { + id: 3, + name: "Project 3", + description: "Description 3", + owner: "Owner 3", + }, + // Add more mock projects as neededects as needed + ]; const handleProjectClick = (projectName: string): void => { setSelectedProject(projectName); @@ -36,7 +58,7 @@ function UserProjectPage(): JSX.Element {
    {projects.map((project, index) => ( { handleProjectClick(project.name); }} @@ -53,7 +75,7 @@ function UserProjectPage(): JSX.Element { const buttons = <>; - return ; + return ; } export default UserProjectPage; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 193b692..1c39ae9 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -29,12 +29,14 @@ import AdminProjectManageMembers from "./Pages/AdminPages/AdminProjectManageMemb import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx"; import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx"; import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx"; +import NotFoundPage from "./Pages/NotFoundPage.tsx"; // This is where the routes are mounted const router = createBrowserRouter([ { path: "/", element: , + errorElement: , }, { path: "/admin", @@ -44,30 +46,26 @@ const router = createBrowserRouter([ path: "/pm", element: , }, - { - path: "/user", - element: , - }, { path: "/yourProjects", element: , }, { - path: "/editTimeReport", - element: , - }, - { - path: "/newTimeReport", - element: , - }, - { - path: "/project", + path: "/project/:projectName", element: , }, { - path: "/projectPage", + path: "/newTimeReport/:projectName", + element: , + }, + { + path: "/projectPage/:projectName", element: , }, + { + path: "/editTimeReport", + element: , + }, { path: "/changeRole", element: , diff --git a/go.work.sum b/go.work.sum index a58340b..087c85b 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,15 +1,28 @@ -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/testing.py b/testing.py index 38d09cc..1eea03b 100644 --- a/testing.py +++ b/testing.py @@ -22,6 +22,27 @@ loginPath = base_url + "/api/login" addProjectPath = base_url + "/api/project" submitReportPath = base_url + "/api/submitReport" getWeeklyReportPath = base_url + "/api/getWeeklyReport" +getProjectPath = base_url + "/api/project" +signReportPath = base_url + "/api/signReport" +addUserToProjectPath = base_url + "/api/addUserToProject" +promoteToAdminPath = base_url + "/api/promoteToAdmin" +getUserProjectsPath = base_url + "/api/getUserProjects" + + +def test_get_user_projects(): + + print("Testing get user projects") + loginResponse = login("user2", "123") + # Check if the user is added to the project + response = requests.get( + getUserProjectsPath, + json={"username": "user2"}, + headers={"Authorization": "Bearer " + loginResponse.json()["token"]}, + ) + print(response.text) + print(response.json()) + assert response.status_code == 200, "Get user projects failed" + print("got user projects successfully") # Posts the username and password to the register endpoint @@ -44,6 +65,7 @@ def login(username: string, password: string): return response +# Test function to login def test_login(): response = login(username, "always_same") assert response.status_code == 200, "Login failed" @@ -51,12 +73,14 @@ def test_login(): return response.json()["token"] +# Test function to create a new user def test_create_user(): response = register(username, "always_same") assert response.status_code == 200, "Registration failed" print("Registration successful") +# Test function to add a project def test_add_project(): loginResponse = login(username, "always_same") token = loginResponse.json()["token"] @@ -70,6 +94,7 @@ def test_add_project(): print("Add project successful") +# Test function to submit a report def test_submit_report(): token = login(username, "always_same").json()["token"] response = requests.post( @@ -90,18 +115,164 @@ def test_submit_report(): assert response.status_code == 200, "Submit report failed" print("Submit report successful") + +# Test function to get a weekly report def test_get_weekly_report(): token = login(username, "always_same").json()["token"] response = requests.get( getWeeklyReportPath, headers={"Authorization": "Bearer " + token}, - params={"username": username, "projectName": projectName , "week": 1} + params={"username": username, "projectName": projectName, "week": 1}, + ) + print(response.text) + assert response.status_code == 200, "Get weekly report failed" + + +# Tests getting a project by id +def test_get_project(): + token = login(username, "always_same").json()["token"] + response = requests.get( + getProjectPath + "/1", # Assumes that the project with id 1 exists + headers={"Authorization": "Bearer " + token}, + ) + print(response.text) + assert response.status_code == 200, "Get project failed" + + +# Test function to add a user to a project +def test_add_user_to_project(): + # Log in as a site admin + admin_username = randomString() + admin_password = "admin_password" + print( + "Registering with username: ", admin_username, " and password: ", admin_password + ) + response = requests.post( + registerPath, json={"username": admin_username, "password": admin_password} ) print(response.text) + admin_token = login(admin_username, admin_password).json()["token"] + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + admin_token}, + ) + print(response.text) + assert response.status_code == 200, "Promote to site admin failed" + print("Admin promoted to site admin successfully") + + # Create a new user to add to the project + new_user = randomString() + register(new_user, "new_user_password") + + # Add the new user to the project as a member + response = requests.put( + addUserToProjectPath, + json={"projectName": projectName, "username": new_user, "role": "member"}, + headers={"Authorization": "Bearer " + admin_token}, + ) + + print(response.text) + assert response.status_code == 200, "Add user to project failed" + print("Add user to project successful") + + +# Test function to sign a report +def test_sign_report(): + # Create a project manager user + project_manager = randomString() + register(project_manager, "project_manager_password") + + # Register an admin + admin_username = randomString() + admin_password = "admin_password2" + print( + "Registering with username: ", admin_username, " and password: ", admin_password + ) + response = requests.post( + registerPath, json={"username": admin_username, "password": admin_password} + ) + print(response.text) + + # Log in as the admin + admin_token = login(admin_username, admin_password).json()["token"] + response = requests.post( + promoteToAdminPath, + json={"username": admin_username}, + headers={"Authorization": "Bearer " + admin_token}, + ) + + response = requests.put( + addUserToProjectPath, + json={ + "projectName": projectName, + "username": project_manager, + "role": "project_manager", + }, + headers={"Authorization": "Bearer " + admin_token}, + ) + assert response.status_code == 200, "Add project manager to project failed" + print("Project manager added to project successfully") + + # Log in as the project manager + project_manager_token = login(project_manager, "project_manager_password").json()[ + "token" + ] + + # Submit a report for the project + token = login(username, "always_same").json()["token"] + response = requests.post( + submitReportPath, + json={ + "projectName": projectName, + "week": 1, + "developmentTime": 10, + "meetingTime": 5, + "adminTime": 5, + "ownWorkTime": 10, + "studyTime": 10, + "testingTime": 10, + }, + headers={"Authorization": "Bearer " + token}, + ) + assert response.status_code == 200, "Submit report failed" + print("Submit report successful") + + # Retrieve the report ID + response = requests.get( + getWeeklyReportPath, + headers={"Authorization": "Bearer " + token}, + params={"username": username, "projectName": projectName, "week": 1}, + ) + print(response.text) + report_id = response.json()["reportId"] + + # Sign the report as the project manager + response = requests.post( + signReportPath, + json={"reportId": report_id}, + headers={"Authorization": "Bearer " + project_manager_token}, + ) + assert response.status_code == 200, "Sign report failed" + print("Sign report successful") + + # Retrieve the report ID again for confirmation + response = requests.get( + getWeeklyReportPath, + headers={"Authorization": "Bearer " + token}, + params={"username": username, "projectName": projectName, "week": 1}, + ) + print(response.text) + + if __name__ == "__main__": + test_get_user_projects() test_create_user() test_login() test_add_project() test_submit_report() - test_get_weekly_report() \ No newline at end of file + test_get_weekly_report() + test_get_project() + test_sign_report() + test_add_user_to_project()