diff --git a/.gitignore b/.gitignore index 05f913b..281e866 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,10 @@ backend/*.png backend/*.jpg backend/*.svg +/go.work.sum +/package-lock.json +/backend/docs/swagger.json + # Test binary, built with `go test -c` *.test diff --git a/Justfile b/Justfile index cb905e4..90fabf6 100644 --- a/Justfile +++ b/Justfile @@ -23,10 +23,13 @@ load-release file: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/Makefile b/Makefile index 97db62e..51fb206 100644 --- a/Makefile +++ b/Makefile @@ -13,10 +13,13 @@ remove-podman-containers: # Tests every part of the project testall: + cd frontend && npm install cd frontend && npm test cd frontend && npm run lint + cd frontend && npm run build cd backend && make test cd backend && make lint + cd backend && make itest # Cleans up everything related to the project clean: remove-podman-containers diff --git a/backend/Makefile b/backend/Makefile index 331f8d5..0ffc557 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -8,17 +8,19 @@ GOGET = $(GOCMD) get # SQLite database filename DB_FILE = db.sqlite3 +PROC_NAME = ttime_server + # Directory containing migration SQL scripts MIGRATIONS_DIR = internal/database/migrations SAMPLE_DATA_DIR = internal/database/sample_data # Build target build: - $(GOBUILD) -o bin/server main.go + $(GOBUILD) -o bin/$(PROC_NAME) main.go # Run target run: build - ./bin/server + ./bin/$(PROC_NAME) watch: build watchexec -c -w . -r make run @@ -32,11 +34,22 @@ clean: rm -f plantuml.jar rm -f erd.png rm -f config.toml + rm -f database.txt # Test target test: db.sqlite3 $(GOTEST) ./... -count=1 +# Integration test target +.PHONY: itest +itest: + pgrep $(PROC_NAME) && echo "Server already running" && exit 1 || true + make build + ./bin/$(PROC_NAME) >/dev/null 2>&1 & + sleep 1 # Adjust if needed + python ../testing.py + pkill $(PROC_NAME) + # Get dependencies target deps: $(GOGET) -v ./... @@ -92,6 +105,17 @@ default: build docs: swag init -outputTypes go +api: ./docs/swagger.json + rm ../frontend/src/API/GenApi.ts + npx swagger-typescript-api \ + --api-class-name GenApi \ + --path ./docs/swagger.json \ + --output ../frontend/src/API \ + --name GenApi.ts \ + +./docs/swagger.json: + swag init -outputTypes json + .PHONY: docfmt docfmt: swag fmt diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 322c812..7a08b0e 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "logs the user in and returns a jwt token", + "description": "Logs in a user and returns a JWT token", "consumes": [ "application/json" ], "produces": [ - "text/plain" + "application/json" ], "tags": [ - "User" + "Auth" ], - "summary": "login", + "summary": "Login", "parameters": [ { - "description": "login info", - "name": "NewUser", + "description": "User credentials", + "name": "body", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully signed token for user", + "description": "JWT token", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "400": { @@ -71,29 +71,26 @@ const docTemplate = `{ } } }, - "/loginerenew": { + "/loginrenew": { "post": { "security": [ { - "bererToken": [] + "JWT": [] } ], - "description": "renews the users token", - "consumes": [ + "description": "Renews the users token.", + "produces": [ "application/json" ], - "produces": [ - "text/plain" - ], "tags": [ - "User" + "Auth" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "401": { @@ -113,7 +110,12 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "description": "promote chosen user to admin", + "security": [ + { + "JWT": [] + } + ], + "description": "Promote chosen user to site admin", "consumes": [ "application/json" ], @@ -137,13 +139,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully prometed user", + "description": "Successfully promoted user", "schema": { - "type": "json" + "$ref": "#/definitions/types.Token" } }, "400": { - "description": "bad request", + "description": "Bad request", "schema": { "type": "string" } @@ -173,7 +175,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "User" + "Auth" ], "summary": "Register", "parameters": [ @@ -211,6 +213,11 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { + "security": [ + { + "JWT": [] + } + ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -252,12 +259,14 @@ const docTemplate = `{ }, "/users/all": { "get": { - "description": "lists all users", - "consumes": [ - "application/json" + "security": [ + { + "JWT": [] + } ], + "description": "lists all users", "produces": [ - "text/plain" + "application/json" ], "tags": [ "User" @@ -265,9 +274,12 @@ const docTemplate = `{ "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully signed token for user", + "description": "Successfully returned all users", "schema": { - "type": "json" + "type": "array", + "items": { + "type": "string" + } } }, "401": { @@ -291,16 +303,27 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string" + "type": "string", + "example": "password123" }, "username": { + "type": "string", + "example": "username123" + } + } + }, + "types.Token": { + "type": "object", + "properties": { + "token": { "type": "string" } } } }, "securityDefinitions": { - "bererToken": { + "JWT": { + "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index fd8297f..f4c0f6e 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -36,12 +36,13 @@ type Database interface { GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error) + GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) IsProjectManager(username string, projectname string) (bool, error) GetProjectTimes(projectName string) (map[string]int, error) - - + UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error + RemoveProject(projectname string) error } // This struct is a wrapper type that holds the database connection @@ -355,6 +356,51 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { return err } +func (d *Db) GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) { + // Define the SQL query to fetch unsigned reports for a given user + query := ` + SELECT + report_id, + user_id, + project_id, + week, + development_time, + meeting_time, + admin_time, + own_work_time, + study_time, + testing_time, + signed_by + FROM + weekly_reports + WHERE + signed_by IS NULL + AND project_id = (SELECT id FROM projects WHERE name = ?) + ` + + // Execute the query + rows, err := d.Queryx(query, projectName) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var reports []types.WeeklyReport + for rows.Next() { + var report types.WeeklyReport + if err := rows.StructScan(&report); err != nil { + return nil, err + } + reports = append(reports, report) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return reports, nil +} + // 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 @@ -454,6 +500,26 @@ func (d *Db) IsProjectManager(username string, projectname string) (bool, error) return manager, err } +func (d *Db) UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { + query := ` + UPDATE weekly_reports + SET + development_time = ?, + meeting_time = ?, + admin_time = ?, + own_work_time = ?, + study_time = ?, + testing_time = ? + WHERE + user_id = (SELECT id FROM users WHERE username = ?) + AND project_id = (SELECT id FROM projects WHERE name = ?) + AND week = ? + ` + + _, err := d.Exec(query, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime, userName, projectName, week) + return err +} + // MigrateSampleData applies sample data to the database. func (d *Db) MigrateSampleData() error { // Insert sample data @@ -495,34 +561,34 @@ func (d *Db) MigrateSampleData() error { // GetProjectTimes retrieves a map with times per "Activity" for a given project func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { - query := ` + query := ` SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time FROM weekly_reports JOIN projects ON weekly_reports.project_id = projects.id WHERE projects.name = ? ` - rows, err := d.DB.Query(query, projectName) - if err != nil { - return nil, err - } - defer rows.Close() + rows, err := d.DB.Query(query, projectName) + if err != nil { + return nil, err + } + defer rows.Close() totalTime := make(map[string]int) - for rows.Next() { - var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int - if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil { - return nil, err - } + for rows.Next() { + var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int + if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil { + return nil, err + } - totalTime["development"] += developmentTime - totalTime["meeting"] += meetingTime - totalTime["admin"] += adminTime - totalTime["own_work"] += ownWorkTime - totalTime["study"] += studyTime - totalTime["testing"] += testingTime - } + totalTime["development"] += developmentTime + totalTime["meeting"] += meetingTime + totalTime["admin"] += adminTime + totalTime["own_work"] += ownWorkTime + totalTime["study"] += studyTime + totalTime["testing"] += testingTime + } if err := rows.Err(); err != nil { return nil, err @@ -530,3 +596,8 @@ func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { return totalTime, nil } + +func (d *Db) RemoveProject(projectname string) error { + _, err := d.Exec("DELETE FROM projects WHERE name = ?", projectname) + return err +} diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 442bf91..fe3e6cd 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -470,6 +470,47 @@ func TestGetWeeklyReport(t *testing.T) { // Check other fields similarly } +func TestGetUnsignedWeeklyReports(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + err = db.AddWeeklyReport("testproject", "testuser1", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + reports, err := db.GetUnsignedWeeklyReports("testproject") + if err != nil { + t.Error("GetUnsignedWeeklyReports failed:", err) + } + + if reports == nil { + t.Error("Expected non-nil reports, got nil") + } +} + // TestSignWeeklyReport tests SignWeeklyReport function of the database func TestSignWeeklyReport(t *testing.T) { db, err := setupState() @@ -729,90 +770,89 @@ func TestIsProjectManager(t *testing.T) { } } - func TestGetProjectTimes(t *testing.T) { - // Initialize - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - return - } + // Initialize + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + return + } - // Create a user - user := "TeaUser" - password := "Vanilla" - err = db.AddUser(user, password) - if err != nil { - t.Error("AddUser failed:", err) - return - } + // Create a user + user := "TeaUser" + password := "Vanilla" + err = db.AddUser(user, password) + if err != nil { + t.Error("AddUser failed:", err) + return + } - // Create a project - projectName := "ProjectVanilla" - projectDescription := "When tea tastes its best" - err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here - if err != nil { - t.Error("AddProject failed:", err) - return - } + // Create a project + projectName := "ProjectVanilla" + projectDescription := "When tea tastes its best" + err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here + if err != nil { + t.Error("AddProject failed:", err) + return + } - // Tests the func in db.go - totalTime, err := db.GetProjectTimes(projectName) - if err != nil { - t.Error("GetTotalTimePerActivity failed:", err) - return - } + // Tests the func in db.go + totalTime, err := db.GetProjectTimes(projectName) + if err != nil { + t.Error("GetTotalTimePerActivity failed:", err) + return + } - // Check if the totalTime map is not nil - if totalTime == nil { - t.Error("Expected non-nil totalTime map, got nil") - return - } + // Check if the totalTime map is not nil + if totalTime == nil { + t.Error("Expected non-nil totalTime map, got nil") + return + } - // Define the expected valeus - expectedTotalTime := map[string]int{ - "development": 0, - "meeting": 0, - "admin": 0, - "own_work": 0, - "study": 0, - "testing": 0, - } + // Define the expected valeus + expectedTotalTime := map[string]int{ + "development": 0, + "meeting": 0, + "admin": 0, + "own_work": 0, + "study": 0, + "testing": 0, + } - // Compare the expectedTotalTime with the totalTime retrieved from the database - for activity, expectedTime := range expectedTotalTime { - if totalTime[activity] != expectedTime { - t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity]) - } - } + // Compare the expectedTotalTime with the totalTime retrieved from the database + for activity, expectedTime := range expectedTotalTime { + if totalTime[activity] != expectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, expectedTime, totalTime[activity]) + } + } - // Insert some data into the database for different activities - err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5) - if err != nil { - t.Error("Failed to insert data into the database:", err) - return - } + // Insert some data into the database for different activities + err = db.AddWeeklyReport(projectName, user, 1, 1, 3, 2, 1, 4, 5) + if err != nil { + t.Error("Failed to insert data into the database:", err) + return + } newTotalTime, err := db.GetProjectTimes(projectName) - if err != nil { - t.Error("GetTotalTimePerActivity failed:", err) - return - } + if err != nil { + t.Error("GetTotalTimePerActivity failed:", err) + return + } newExpectedTotalTime := map[string]int{ - "development": 1, - "meeting": 3, - "admin": 2, - "own_work": 1, - "study": 4, - "testing": 5, - } + "development": 1, + "meeting": 3, + "admin": 2, + "own_work": 1, + "study": 4, + "testing": 5, + } for activity, newExpectedTime := range newExpectedTotalTime { - if newTotalTime[activity] != newExpectedTime { - t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity]) - } - } + if newTotalTime[activity] != newExpectedTime { + t.Errorf("Expected %s time to be %d, got %d", activity, newExpectedTime, newTotalTime[activity]) + } + } } func TestEnsureManagerOfCreatedProject(t *testing.T) { db, err := setupState() @@ -847,3 +887,81 @@ func TestEnsureManagerOfCreatedProject(t *testing.T) { t.Error("Expected testuser to be a project manager, but it's not.") } } + +// TestUpdateWeeklyReport tests the UpdateWeeklyReport function of the database +func TestUpdateWeeklyReport(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) + } + + // Add a weekly report + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + if err != nil { + t.Error("AddWeeklyReport failed:", err) + } + + // Update the weekly report + err = db.UpdateWeeklyReport("testproject", "testuser", 1, 2, 2, 2, 2, 2, 2) + if err != nil { + t.Error("UpdateWeeklyReport failed:", err) + } + + // Retrieve the updated report + updatedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) + if err != nil { + t.Error("GetWeeklyReport failed:", err) + } + + // Check if the report was updated correctly + if updatedReport.DevelopmentTime != 2 || + updatedReport.MeetingTime != 2 || + updatedReport.AdminTime != 2 || + updatedReport.OwnWorkTime != 2 || + updatedReport.StudyTime != 2 || + updatedReport.TestingTime != 2 { + t.Error("UpdateWeeklyReport failed: report not updated correctly") + } +} + +func TestRemoveProject(t *testing.T) { + db, err := setupAdvancedState() + if err != nil { + t.Error("setupState failed:", err) + } + + // Promote user to Admin + err = db.PromoteToAdmin("demouser") + if err != nil { + t.Error("PromoteToAdmin failed:", err) + } + + // Remove project + err = db.RemoveProject("projecttest") + if err != nil { + t.Error("RemoveProject failed:", err) + } + + // Check if the project was removed + projects, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + if len(projects) != 0 { + t.Error("RemoveProject failed: expected 0, got", len(projects)) + } + +} + \ No newline at end of file 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 b88bdcd..0000000 --- a/backend/internal/handlers/global_state.go +++ /dev/null @@ -1,41 +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 -} - -// "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_project_related.go b/backend/internal/handlers/handlers_project_related.go deleted file mode 100644 index e9ef966..0000000 --- a/backend/internal/handlers/handlers_project_related.go +++ /dev/null @@ -1,236 +0,0 @@ -package handlers - -import ( - "strconv" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -// CreateProject is a simple handler that creates a new project -func (gs *GState) CreateProject(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - p := new(types.NewProject) - if err := c.BodyParser(p); err != nil { - return c.Status(400).SendString(err.Error()) - } - - // Get the username from the token and set it as the owner of the project - // This is ugly but - claims := user.Claims.(jwt.MapClaims) - owner := claims["name"].(string) - - if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Project added") -} - -func (gs *GState) DeleteProject(c *fiber.Ctx) error { - - projectID := c.Params("projectID") - username := c.Params("username") - - if err := gs.Db.DeleteProject(projectID, username); err != nil { - return c.Status(500).SendString((err.Error())) - } - - return c.Status(200).SendString("Project deleted") -} - -// GetUserProjects returns all projects that the user is a member of -func (gs *GState) GetUserProjects(c *fiber.Ctx) error { - username := c.Params("username") - if username == "" { - log.Info("No username provided") - return c.Status(400).SendString("No username provided") - } - - // Then dip into the database to get the projects - projects, err := gs.Db.GetProjectsForUser(username) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a json serialized list of projects - return c.JSON(projects) -} - -// ProjectRoleChange is a handler that changes a user's role within a project -func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { - - //check token and get username of current user - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract the necessary parameters from the request - data := new(types.RoleChange) - if err := c.BodyParser(data); err != nil { - log.Info("error parsing username, project or role") - return c.Status(400).SendString(err.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 { - log.Warn("Error checking if projectmanager:", err) - return c.Status(500).SendString(err.Error()) - } else if !ismanager { - log.Warn("User is not projectmanager") - return c.Status(401).SendString("User is not projectmanager") - } - - // Change the user's role within the project in the database - if err := gs.Db.ChangeUserRole(username, data.Projectname, data.Role); err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} - -// GetProject retrieves a specific project by its ID -func (gs *GState) GetProject(c *fiber.Ctx) error { - // Extract the project ID from the request parameters or body - projectID := c.Params("projectID") - if projectID == "" { - 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 the user token - userToken := c.Locals("user").(*jwt.Token) - claims := userToken.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Check if the user is a project manager for the specified project - isManager, err := gs.Db.IsProjectManager(username, projectName) - if err != nil { - log.Info("Error checking project manager status:", err) - return c.Status(500).SendString(err.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) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - if !isAdmin { - log.Info("User is neither a project manager nor a site admin:", username) - return c.Status(403).SendString("User is neither a project manager nor a site admin") - } - } - - // 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) -} - -// 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 { - // Get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract necessary parameters from the request query string - projectName := c.Params("projectName") - - 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) - if err != nil { - log.Info("Error checking project manager status:", err) - return c.Status(500).SendString(err.Error()) - } - - // Return the result as JSON - return c.JSON(fiber.Map{"isProjectManager": isManager}) -} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go deleted file mode 100644 index fcba523..0000000 --- a/backend/internal/handlers/handlers_report_related.go +++ /dev/null @@ -1,143 +0,0 @@ -package handlers - -import ( - "strconv" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" - "github.com/golang-jwt/jwt/v5" -) - -func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - report := new(types.NewWeeklyReport) - if err := c.BodyParser(report); err != nil { - 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 to db:", err) - 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 - 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") - week := c.Query("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) - projectManagerUsername := claims["name"].(string) - - 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) - - // 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(rid.ReportId, projectManagerID) - if err != nil { - log.Info("Error signing weekly report:", err) - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Weekly report signed successfully") -} - -// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project -func (gs *GState) GetWeeklyReportsUserHandler(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract necessary (path) parameters from the request - projectName := c.Params("projectName") - - // TODO: Here we need to check whether the user is a member of the project - // If not, we should return an error. On the other hand, if the user not a member, - // 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) - if err != nil { - log.Error("Error getting weekly reports for user:", username, "in project:", projectName, ":", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning weekly reports for user:", username, "in project:", projectName) - - // Return the list of reports as JSON - return c.JSON(reports) -} 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/projects/AddUserToProject.go b/backend/internal/handlers/projects/AddUserToProject.go new file mode 100644 index 0000000..702b7dd --- /dev/null +++ b/backend/internal/handlers/projects/AddUserToProject.go @@ -0,0 +1,51 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// AddUserToProjectHandler is a handler that adds a user to a project with a specified role +func 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 := db.GetDb(c).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 = 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()) + } + + // Return success message + log.Info("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/projects/CreateProject.go b/backend/internal/handlers/projects/CreateProject.go new file mode 100644 index 0000000..cef2f2b --- /dev/null +++ b/backend/internal/handlers/projects/CreateProject.go @@ -0,0 +1,30 @@ +package projects + +import ( + db "ttime/internal/database" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// CreateProject is a simple handler that creates a new project +func CreateProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + p := new(types.NewProject) + if err := c.BodyParser(p); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Get the username from the token and set it as the owner of the project + // This is ugly but + claims := user.Claims.(jwt.MapClaims) + owner := claims["name"].(string) + + if err := 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") +} diff --git a/backend/internal/handlers/projects/DeleteProject.go b/backend/internal/handlers/projects/DeleteProject.go new file mode 100644 index 0000000..415424a --- /dev/null +++ b/backend/internal/handlers/projects/DeleteProject.go @@ -0,0 +1,19 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" +) + +func DeleteProject(c *fiber.Ctx) error { + + projectID := c.Params("projectID") + username := c.Params("username") + + if err := db.GetDb(c).DeleteProject(projectID, username); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} diff --git a/backend/internal/handlers/projects/GetProject.go b/backend/internal/handlers/projects/GetProject.go new file mode 100644 index 0000000..03333ce --- /dev/null +++ b/backend/internal/handlers/projects/GetProject.go @@ -0,0 +1,38 @@ +package projects + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// GetProject retrieves a specific project by its ID +func 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 := db.GetDb(c).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) +} diff --git a/backend/internal/handlers/projects/GetProjectTimes.go b/backend/internal/handlers/projects/GetProjectTimes.go new file mode 100644 index 0000000..573a95e --- /dev/null +++ b/backend/internal/handlers/projects/GetProjectTimes.go @@ -0,0 +1,63 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func GetProjectTimesHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Get project + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get all users in the project and roles + 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()) + } + + // If the user is member + isMember := false + for _, userProject := range userProjects { + if userProject.Username == username { + isMember = true + break + } + } + + // If the user is admin + if !isMember { + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project member nor a site admin:", username) + return c.Status(403).SendString("User is neither a project member nor a site admin") + } + } + + // Get project times + projectTimes, err := db.GetDb(c).GetProjectTimes(projectName) + if err != nil { + log.Info("Error getting project times:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return project times as JSON + log.Info("Returning project times for project:", projectName) + return c.JSON(projectTimes) +} diff --git a/backend/internal/handlers/projects/GetUserProject.go b/backend/internal/handlers/projects/GetUserProject.go new file mode 100644 index 0000000..99ed63b --- /dev/null +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -0,0 +1,25 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" +) + +// GetUserProjects returns all projects that the user is a member of +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 := db.GetDb(c).GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} diff --git a/backend/internal/handlers/projects/IsProjectManager.go b/backend/internal/handlers/projects/IsProjectManager.go new file mode 100644 index 0000000..678fad5 --- /dev/null +++ b/backend/internal/handlers/projects/IsProjectManager.go @@ -0,0 +1,32 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// IsProjectManagerHandler is a handler that checks if a user is a project manager for a given project +func IsProjectManagerHandler(c *fiber.Ctx) error { + // Get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract necessary parameters from the request query string + projectName := c.Params("projectName") + + 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 := db.GetDb(c).IsProjectManager(username, projectName) + if err != nil { + log.Info("Error checking project manager status:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return the result as JSON + return c.JSON(fiber.Map{"isProjectManager": isManager}) +} diff --git a/backend/internal/handlers/projects/ListAllUserProjects.go b/backend/internal/handlers/projects/ListAllUserProjects.go new file mode 100644 index 0000000..e0bcaf5 --- /dev/null +++ b/backend/internal/handlers/projects/ListAllUserProjects.go @@ -0,0 +1,55 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func 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 the user token + userToken := c.Locals("user").(*jwt.Token) + claims := userToken.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Check if the user is a project manager for the specified project + 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()) + } + + // If the user is not a project manager, check if the user is a site admin + if !isManager { + isAdmin, err := db.GetDb(c).IsSiteAdmin(username) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + if !isAdmin { + log.Info("User is neither a project manager nor a site admin:", username) + return c.Status(403).SendString("User is neither a project manager nor a site admin") + } + } + + // Get all users associated with the project from the database + 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()) + } + + log.Info("Returning users for project: ", projectName) + + // Return the list of users as JSON + return c.JSON(users) +} diff --git a/backend/internal/handlers/projects/ProjectRoleChange.go b/backend/internal/handlers/projects/ProjectRoleChange.go new file mode 100644 index 0000000..266127d --- /dev/null +++ b/backend/internal/handlers/projects/ProjectRoleChange.go @@ -0,0 +1,45 @@ +package projects + +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" +) + +// ProjectRoleChange is a handler that changes a user's role within a project +func ProjectRoleChange(c *fiber.Ctx) error { + + //check token and get username of current user + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract the necessary parameters from the request + data := new(types.RoleChange) + if err := c.BodyParser(data); err != nil { + log.Info("error parsing username, project or role") + return c.Status(400).SendString(err.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 := 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 { + log.Warn("User is not projectmanager") + return c.Status(401).SendString("User is not projectmanager") + } + + // Change the user's role within the project in the database + if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/projects/RemoveProject.go b/backend/internal/handlers/projects/RemoveProject.go new file mode 100644 index 0000000..7b140dd --- /dev/null +++ b/backend/internal/handlers/projects/RemoveProject.go @@ -0,0 +1,35 @@ +package projects + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +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 := db.GetDb(c).IsSiteAdmin(username) + 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:", username) + return c.Status(403).SendString("User is not a site admin") + } + + projectName := c.Params("projectName") + + if err := db.GetDb(c).RemoveProject(projectName); err != nil { + return c.Status(500).SendString((err.Error())) + } + + return c.Status(200).SendString("Project deleted") +} diff --git a/backend/internal/handlers/reports/GetUnsignedReports.go b/backend/internal/handlers/reports/GetUnsignedReports.go new file mode 100644 index 0000000..9525f55 --- /dev/null +++ b/backend/internal/handlers/reports/GetUnsignedReports.go @@ -0,0 +1,45 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func GetUnsignedReports(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + projectManagerUsername := claims["name"].(string) + + // Extract project name and week from query parameters + projectName := c.Params("projectName") + + log.Info("Getting unsigned reports for") + + if projectName == "" { + log.Info("Missing project name") + return c.Status(400).SendString("Missing project name") + } + + // Get the project manager's ID + 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") + } + log.Info("User is Project Manager: ", isProjectManager) + + // Call the database function to get the unsigned weekly reports + 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()) + } + + log.Info("Returning unsigned reports") + // Return the list of unsigned reports + return c.JSON(reports) +} diff --git a/backend/internal/handlers/reports/GetWeeklyReport.go b/backend/internal/handlers/reports/GetWeeklyReport.go new file mode 100644 index 0000000..422bc0b --- /dev/null +++ b/backend/internal/handlers/reports/GetWeeklyReport.go @@ -0,0 +1,47 @@ +package reports + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// Handler for retrieving weekly report +func GetWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + log.Info("Getting weekly report for: ", username) + + // Extract project name and week from query parameters + projectName := c.Query("projectName") + week := c.Query("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 := 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()) + } + + log.Info("Returning weekly report") + // Return the retrieved weekly report + return c.JSON(report) +} diff --git a/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go b/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go new file mode 100644 index 0000000..da8a90b --- /dev/null +++ b/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go @@ -0,0 +1,36 @@ +package reports + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// GetWeeklyReportsUserHandler retrieves all weekly reports for a user in a specific project +func GetWeeklyReportsUserHandler(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Extract necessary (path) parameters from the request + projectName := c.Params("projectName") + + // TODO: Here we need to check whether the user is a member of the project + // If not, we should return an error. On the other hand, if the user not a member, + // the returned list of reports will (should) allways be empty. + + // Retrieve weekly reports for the user in the project from the database + 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()) + } + + log.Info("Returning weekly reports for user:", username, "in project:", projectName) + + // Return the list of reports as JSON + return c.JSON(reports) +} diff --git a/backend/internal/handlers/reports/SignReport.go b/backend/internal/handlers/reports/SignReport.go new file mode 100644 index 0000000..a486ecc --- /dev/null +++ b/backend/internal/handlers/reports/SignReport.go @@ -0,0 +1,41 @@ +package reports + +import ( + "strconv" + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +func SignReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + projectManagerUsername := claims["name"].(string) + + // Extract report ID from the path + reportId, err := strconv.Atoi(c.Params("reportId")) + if err != nil { + log.Info("Invalid report ID") + return c.Status(400).SendString("Invalid report ID") + } + + // Get the project manager's ID + projectManagerID, err := db.GetDb(c).GetUserId(projectManagerUsername) + if err != nil { + log.Info("Failed to get project manager ID for user: ", projectManagerUsername) + return c.Status(500).SendString("Failed to get project manager ID") + } + + // Call the database function to sign the weekly report + err = db.GetDb(c).SignWeeklyReport(reportId, projectManagerID) + if err != nil { + log.Info("Error signing weekly report:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Project manager ID: ", projectManagerID, " signed report ID: ", reportId) + return c.Status(200).SendString("Weekly report signed successfully") +} diff --git a/backend/internal/handlers/reports/SubmitWeeklyReport.go b/backend/internal/handlers/reports/SubmitWeeklyReport.go new file mode 100644 index 0000000..900aa03 --- /dev/null +++ b/backend/internal/handlers/reports/SubmitWeeklyReport.go @@ -0,0 +1,41 @@ +package reports + +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" +) + +func SubmitWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + report := new(types.NewWeeklyReport) + if err := c.BodyParser(report); err != nil { + 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 := 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()) + } + + log.Info("Weekly report added") + return c.Status(200).SendString("Time report added") +} diff --git a/backend/internal/handlers/reports/UpdateWeeklyReport.go b/backend/internal/handlers/reports/UpdateWeeklyReport.go new file mode 100644 index 0000000..3ab835d --- /dev/null +++ b/backend/internal/handlers/reports/UpdateWeeklyReport.go @@ -0,0 +1,44 @@ +package reports + +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" +) + +func UpdateWeeklyReport(c *fiber.Ctx) error { + // Extract the necessary parameters from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Parse the request body into an UpdateWeeklyReport struct + var updateReport types.UpdateWeeklyReport + if err := c.BodyParser(&updateReport); 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 updateReport.Week < 1 || updateReport.Week > 52 { + log.Info("Invalid week number") + return c.Status(400).SendString("Invalid week number") + } + + if updateReport.DevelopmentTime < 0 || updateReport.MeetingTime < 0 || updateReport.AdminTime < 0 || updateReport.OwnWorkTime < 0 || updateReport.StudyTime < 0 || updateReport.TestingTime < 0 { + log.Info("Invalid time report") + return c.Status(400).SendString("Invalid time report") + } + + // Update the weekly report in the database + 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()) + } + + log.Info("Weekly report updated") + return c.Status(200).SendString("Weekly report updated") +} 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..5ac5df0 --- /dev/null +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -0,0 +1,32 @@ +package users + +import ( + db "ttime/internal/database" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" +) + +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Produce json +// @Security JWT +// @Success 200 {array} string "Successfully returned all users" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] +// +// ListAllUsers returns a list of all users in the application database +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..42c52a5 --- /dev/null +++ b/backend/internal/handlers/users/Login.go @@ -0,0 +1,66 @@ +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" +) + +// @Summary Login +// @Description Logs in a user and returns a JWT token +// @Tags Auth +// @Accept json +// @Produce json +// @Param body body types.NewUser true "User credentials" +// @Success 200 {object} types.Token "JWT token" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] +// +// Login logs in a user and returns a JWT token +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..3926ce4 --- /dev/null +++ b/backend/internal/handlers/users/LoginRenew.go @@ -0,0 +1,50 @@ +package users + +import ( + "time" + "ttime/internal/types" + + "github.com/gofiber/fiber/v2" + "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" +) + +// @Summary LoginRenews +// @Description Renews the users token. +// @Tags Auth +// @Produce json +// @Security JWT +// @Success 200 {object} types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginrenew [post] +// +// LoginRenew renews the users token +func LoginRenew(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) + + // Renewing the token means we trust whatever is already in the token + claims := user.Claims.(jwt.MapClaims) + + // 72 hour expiration time + claims["exp"] = time.Now().Add(time.Hour * 72).Unix() + + // Create token with old claims, but new expiration time + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "name": claims["name"], + "admin": claims["admin"], + "exp": claims["exp"], + }) + + // Sign it with top secret key + t, err := token.SignedString([]byte("secret")) + if err != nil { + log.Warn("Error signing token") + return c.SendStatus(fiber.StatusInternalServerError) // 500 + } + + 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..3f0a6d3 --- /dev/null +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -0,0 +1,45 @@ +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 site admin +// @Tags User +// @Accept json +// @Produce plain +// @Security JWT +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {object} types.Token "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] +// +// PromoteToAdmin promotes a user to a site admin +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..b9e0c78 --- /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" +) + +// @Summary Register +// @Description Register a new user +// @Tags Auth +// @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] +// +// Register is a simple handler that registers a new user +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..491a1b3 --- /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" +) + +// @Summary UserDelete +// @Description UserDelete deletes a user from the database +// @Tags User +// @Accept json +// @Produce plain +// @Security JWT +// @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] +// +// UserDelete deletes a user from the database +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/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 8d22b6a..234781b 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -65,3 +65,24 @@ type WeeklyReport struct { // The project manager who signed it SignedBy *int `json:"signedBy" db:"signed_by"` } + +type UpdateWeeklyReport struct { + // The name of the project, as it appears in the database + ProjectName string `json:"projectName"` + // The name of the user + UserName string `json:"userName"` + // The week number + Week int `json:"week"` + // Total time spent on development + DevelopmentTime int `json:"developmentTime"` + // Total time spent in meetings + MeetingTime int `json:"meetingTime"` + // Total time spent on administrative tasks + AdminTime int `json:"adminTime"` + // Total time spent on personal projects + OwnWorkTime int `json:"ownWorkTime"` + // Total time spent on studying + StudyTime int `json:"studyTime"` + // Total time spent on testing + TestingTime int `json:"testingTime"` +} diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 88b4f06..37cc8c2 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) { // Should be used when registering, for example type NewUser struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username" example:"username123"` + Password string `json:"password" example:"password123"` } // PublicUser represents a user that is safe to send over the API (no password) diff --git a/backend/main.go b/backend/main.go index 7d98918..cf58280 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" @@ -23,15 +25,22 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -//@securityDefinitions.apikey bererToken -//@in header -//@name Authorization +// @securityDefinitions.apikey JWT +// @in header +// @name Authorization +// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".** // @host localhost:8080 // @BasePath /api -// @externalDocs.description OpenAPI -// @externalDocs.url https://swagger.io/resources/open-api/ +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ + +/** +Main function for starting the server and initializing configurations. +Reads configuration from file, pretty prints it, connects to the database, +migrates it, and sets up routes for the server. +*/ func main() { conf, err := config.ReadConfigFromFile("config.toml") @@ -48,24 +57,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) @@ -73,34 +86,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/:username", 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.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) + // All user related routes + // userGroup := api.Group("/user") // Not currently in use + api.Get("/users/all", users.ListAllUsers) + api.Get("/project/getAllUsers", users.GetAllUsersProject) + api.Post("/login", users.Login) + api.Post("/register", users.Register) + api.Post("/loginrenew", users.LoginRenew) + api.Post("/promoteToAdmin", users.PromoteToAdmin) + api.Put("/changeUserName", users.ChangeUserName) + api.Delete("/userdelete/:username", users.UserDelete) // Perhaps just use POST to avoid headaches + + // All project related routes + // projectGroup := api.Group("/project") // Not currently in use + api.Get("/getUserProjects", projects.GetUserProjects) + api.Get("/project/:projectId", projects.GetProject) + api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) + api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) + api.Post("/project", projects.CreateProject) + api.Post("/ProjectRoleChange", projects.ProjectRoleChange) + api.Delete("/removeProject/:projectName", projects.RemoveProject) + api.Delete("/project/:projectID", projects.DeleteProject) + + // All report related routes + // reportGroup := api.Group("/report") // Not currently in use + api.Get("/getWeeklyReport", reports.GetWeeklyReport) + api.Get("/getUnsignedReports/:projectName", reports.GetUnsignedReports) + api.Get("/getWeeklyReportsUser/:projectName", reports.GetWeeklyReportsUserHandler) + api.Post("/submitWeeklyReport", reports.SubmitWeeklyReport) + api.Put("/signReport/:reportId", reports.SignReport) + api.Put("/addUserToProject", projects.AddUserToProjectHandler) + api.Put("/updateWeeklyReport", reports.UpdateWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/container/Containerfile b/container/Containerfile index ecd2f84..f9cb39d 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -13,7 +13,6 @@ FROM docker.io/golang:alpine as go RUN apk add gcompat RUN apk add gcc RUN apk add musl-dev -RUN apk add make RUN apk add sqlite WORKDIR /build ADD backend/go.mod backend/go.sum ./ @@ -24,9 +23,7 @@ RUN go mod download # Add the source code ADD backend . -RUN make migrate - -# RUN go build -o server +RUN go build -o server RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o ./server ./main.go # Strip the binary for a smaller image @@ -37,6 +34,7 @@ FROM docker.io/alpine:latest as runner RUN adduser -D nonroot RUN addgroup nonroot nonroot WORKDIR /app +RUN chown nonroot:nonroot /app # Copy the frontend SPA build into public COPY --from=client /build/dist static @@ -44,9 +42,6 @@ COPY --from=client /build/dist static # Copy the server binary COPY --from=go /build/server server -# Copy the database -COPY --from=go /build/db.sqlite3 db.sqlite3 - # Expose port 8080 EXPOSE 8080 diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..c49d006 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +goTypes.ts +GenApi.ts \ No newline at end of file diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 39b5d0a..e7de646 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -49,7 +49,6 @@ interface API { * @returns {Promise>} A promise containing the API response indicating if the user is a project manager. */ checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise>; @@ -87,7 +86,7 @@ interface API { submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise>; + ): Promise>; /** Gets a weekly report for a specific user, project and week * @param {string} projectName The name of the project. @@ -153,6 +152,23 @@ interface API { user: NewProjMember, token: string, ): Promise>; + + removeProject( + projectName: string, + token: string, + ): Promise>; + + /** + * Signs a report. Keep in mind that the user which the token belongs to must be + * the project manager of the project the report belongs to. + * + * @param {number} reportId The id of the report to sign + * @param {string} token The authentication token + */ + signReport( + reportId: number, + token: string, + ): Promise>; } /** An instance of the API */ @@ -208,19 +224,20 @@ export const api: API = { }, async checkIfProjectManager( - username: string, projectName: string, token: string, ): Promise> { try { - const response = await fetch("/api/checkIfProjectManager", { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, + const response = await fetch( + `/api/checkIfProjectManager/${projectName}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, }, - body: JSON.stringify({ username, projectName }), - }); + ); if (!response.ok) { return { @@ -232,7 +249,7 @@ export const api: API = { return { success: true, data }; } } catch (e) { - return { success: false, message: "fuck" }; + return { success: false, message: "Failed to check if project manager" }; } }, @@ -339,7 +356,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -357,8 +374,8 @@ export const api: API = { }; } - const data = (await response.json()) as NewWeeklyReport; - return { success: true, data }; + const data = await response.text(); + return { success: true, message: data }; } catch (e) { return { success: false, @@ -553,4 +570,57 @@ export const api: API = { return { success: false, message: "Failed to change username" }; } }, + + async removeProject( + projectName: string, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/projectdelete/${projectName}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } else { + const data = await response.text(); + return Promise.resolve({ success: true, message: data }); + } + } catch (e) { + return Promise.resolve({ + success: false, + message: "Failed to remove project", + }); + } + }, + + async signReport( + reportId: number, + token: string, + ): Promise> { + try { + const response = await fetch(`/api/signReport/${reportId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }); + + if (!response.ok) { + return { success: false, message: "Failed to sign report" }; + } else { + return { success: true, message: "Report signed" }; + } + } catch (e) { + return { success: false, message: "Failed to sign report" }; + } + } }; diff --git a/frontend/src/API/GenApi.ts b/frontend/src/API/GenApi.ts new file mode 100644 index 0000000..8ca851b --- /dev/null +++ b/frontend/src/API/GenApi.ts @@ -0,0 +1,358 @@ +/* eslint-disable */ +/* tslint:disable */ +/* + * --------------------------------------------------------------- + * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## + * ## ## + * ## AUTHOR: acacode ## + * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## + * --------------------------------------------------------------- + */ + +export interface TypesNewUser { + /** @example "password123" */ + password?: string; + /** @example "username123" */ + username?: string; +} + +export interface TypesToken { + token?: string; +} + +export type QueryParamsType = Record; +export type ResponseFormat = keyof Omit; + +export interface FullRequestParams extends Omit { + /** set parameter to `true` for call `securityWorker` for this request */ + secure?: boolean; + /** request path */ + path: string; + /** content type of request body */ + type?: ContentType; + /** query params */ + query?: QueryParamsType; + /** format of response (i.e. response.json() -> format: "json") */ + format?: ResponseFormat; + /** request body */ + body?: unknown; + /** base url */ + baseUrl?: string; + /** request cancellation token */ + cancelToken?: CancelToken; +} + +export type RequestParams = Omit; + +export interface ApiConfig { + baseUrl?: string; + baseApiParams?: Omit; + securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; + customFetch?: typeof fetch; +} + +export interface HttpResponse extends Response { + data: D; + error: E; +} + +type CancelToken = Symbol | string | number; + +export enum ContentType { + Json = "application/json", + FormData = "multipart/form-data", + UrlEncoded = "application/x-www-form-urlencoded", + Text = "text/plain", +} + +export class HttpClient { + public baseUrl: string = "//localhost:8080/api"; + private securityData: SecurityDataType | null = null; + private securityWorker?: ApiConfig["securityWorker"]; + private abortControllers = new Map(); + private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); + + private baseApiParams: RequestParams = { + credentials: "same-origin", + headers: {}, + redirect: "follow", + referrerPolicy: "no-referrer", + }; + + constructor(apiConfig: ApiConfig = {}) { + Object.assign(this, apiConfig); + } + + public setSecurityData = (data: SecurityDataType | null) => { + this.securityData = data; + }; + + protected encodeQueryParam(key: string, value: any) { + const encodedKey = encodeURIComponent(key); + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + } + + protected addQueryParam(query: QueryParamsType, key: string) { + return this.encodeQueryParam(key, query[key]); + } + + protected addArrayQueryParam(query: QueryParamsType, key: string) { + const value = query[key]; + return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); + } + + protected toQueryString(rawQuery?: QueryParamsType): string { + const query = rawQuery || {}; + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + return keys + .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) + .join("&"); + } + + protected addQueryParams(rawQuery?: QueryParamsType): string { + const queryString = this.toQueryString(rawQuery); + return queryString ? `?${queryString}` : ""; + } + + private contentFormatters: Record any> = { + [ContentType.Json]: (input: any) => + input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, + [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), + [ContentType.FormData]: (input: any) => + Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + formData.append( + key, + property instanceof Blob + ? property + : typeof property === "object" && property !== null + ? JSON.stringify(property) + : `${property}`, + ); + return formData; + }, new FormData()), + [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), + }; + + protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { + return { + ...this.baseApiParams, + ...params1, + ...(params2 || {}), + headers: { + ...(this.baseApiParams.headers || {}), + ...(params1.headers || {}), + ...((params2 && params2.headers) || {}), + }, + }; + } + + protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { + if (this.abortControllers.has(cancelToken)) { + const abortController = this.abortControllers.get(cancelToken); + if (abortController) { + return abortController.signal; + } + return void 0; + } + + const abortController = new AbortController(); + this.abortControllers.set(cancelToken, abortController); + return abortController.signal; + }; + + public abortRequest = (cancelToken: CancelToken) => { + const abortController = this.abortControllers.get(cancelToken); + + if (abortController) { + abortController.abort(); + this.abortControllers.delete(cancelToken); + } + }; + + public request = async ({ + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }: FullRequestParams): Promise> => { + const secureParams = + ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && + this.securityWorker && + (await this.securityWorker(this.securityData))) || + {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const queryString = query && this.toQueryString(query); + const payloadFormatter = this.contentFormatters[type || ContentType.Json]; + const responseFormat = format || requestParams.format; + + return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { + ...requestParams, + headers: { + ...(requestParams.headers || {}), + ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), + }, + signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, + body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), + }).then(async (response) => { + const r = response as HttpResponse; + r.data = null as unknown as T; + r.error = null as unknown as E; + + const data = !responseFormat + ? r + : await response[responseFormat]() + .then((data) => { + if (r.ok) { + r.data = data; + } else { + r.error = data; + } + return r; + }) + .catch((e) => { + r.error = e; + return r; + }); + + if (cancelToken) { + this.abortControllers.delete(cancelToken); + } + + if (!response.ok) throw data; + return data; + }); + }; +} + +/** + * @title TTime API + * @version 0.0.1 + * @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html) + * @baseUrl //localhost:8080/api + * @externalDocs https://swagger.io/resources/open-api/ + * @contact + * + * This is the API for TTime, a time tracking application. + */ +export class GenApi extends HttpClient { + login = { + /** + * @description Logs in a user and returns a JWT token + * + * @tags Auth + * @name LoginCreate + * @summary Login + * @request POST:/login + */ + loginCreate: (body: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/login`, + method: "POST", + body: body, + type: ContentType.Json, + format: "json", + ...params, + }), + }; + loginrenew = { + /** + * @description Renews the users token. + * + * @tags Auth + * @name LoginrenewCreate + * @summary LoginRenews + * @request POST:/loginrenew + * @secure + */ + loginrenewCreate: (params: RequestParams = {}) => + this.request({ + path: `/loginrenew`, + method: "POST", + secure: true, + format: "json", + ...params, + }), + }; + promoteToAdmin = { + /** + * @description Promote chosen user to site admin + * + * @tags User + * @name PromoteToAdminCreate + * @summary PromoteToAdmin + * @request POST:/promoteToAdmin + * @secure + */ + promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/promoteToAdmin`, + method: "POST", + body: NewUser, + secure: true, + type: ContentType.Json, + ...params, + }), + }; + register = { + /** + * @description Register a new user + * + * @tags Auth + * @name RegisterCreate + * @summary Register + * @request POST:/register + */ + registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => + this.request({ + path: `/register`, + method: "POST", + body: NewUser, + type: ContentType.Json, + ...params, + }), + }; + userdelete = { + /** + * @description UserDelete deletes a user from the database + * + * @tags User + * @name UserdeleteDelete + * @summary UserDelete + * @request DELETE:/userdelete/{username} + * @secure + */ + userdeleteDelete: (username: string, params: RequestParams = {}) => + this.request({ + path: `/userdelete/${username}`, + method: "DELETE", + secure: true, + type: ContentType.Json, + ...params, + }), + }; + users = { + /** + * @description lists all users + * + * @tags User + * @name GetUsers + * @summary ListsAllUsers + * @request GET:/users/all + * @secure + */ + getUsers: (params: RequestParams = {}) => + this.request({ + path: `/users/all`, + method: "GET", + secure: true, + format: "json", + ...params, + }), + }; +} diff --git a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx new file mode 100644 index 0000000..09ca6dc --- /dev/null +++ b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx @@ -0,0 +1,103 @@ +//Info: This component is used to display all the time reports for a project. It will display the week number, +//total time spent, and if the report has been signed or not. The user can click on a report to edit it. +import { useEffect, useState } from "react"; +import { NewWeeklyReport } from "../Types/goTypes"; +import { Link, useParams } from "react-router-dom"; + +/** + * Renders a component that displays all the time reports for a specific project. + * @returns {JSX.Element} representing the component. + */ +function AllTimeReportsInProject(): JSX.Element { + const { username } = useParams(); + const { projectName } = useParams(); + const [weeklyReports, setWeeklyReports] = useState([]); + + /* // Call getProjects when the component mounts + useEffect(() => { + const getWeeklyReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getWeeklyReportsForUser( + projectName ?? "", + token, + ); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } + }; */ + // Mock data + const getWeeklyReports = async (): Promise => { + // Simulate a delay + await Promise.resolve(); + const mockWeeklyReports: NewWeeklyReport[] = [ + { + projectName: "Project 1", + week: 1, + developmentTime: 10, + meetingTime: 2, + adminTime: 1, + ownWorkTime: 3, + studyTime: 4, + testingTime: 5, + }, + { + projectName: "Project 1", + week: 2, + developmentTime: 8, + meetingTime: 2, + adminTime: 1, + ownWorkTime: 3, + studyTime: 4, + testingTime: 5, + }, + // Add more reports as needed + ]; + + // Use the mock data instead of the real data + setWeeklyReports(mockWeeklyReports); + }; + useEffect(() => { + void getWeeklyReports(); + }, []); + + return ( + <> +

{username}'s Time Reports

+
+ {weeklyReports.map((newWeeklyReport, index) => ( + +
+

+ {"Week: "} + {newWeeklyReport.week} +

+

+ {"Total Time: "} + {newWeeklyReport.developmentTime + + newWeeklyReport.meetingTime + + newWeeklyReport.adminTime + + newWeeklyReport.ownWorkTime + + newWeeklyReport.studyTime + + newWeeklyReport.testingTime}{" "} + min +

+

+ {"Signed: "} + NO +

+
+ + ))} +
+ + ); +} + +export default AllTimeReportsInProject; diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx new file mode 100644 index 0000000..780f20c --- /dev/null +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -0,0 +1,129 @@ +import { useState, useEffect } from "react"; +import { Link, useParams } from "react-router-dom"; + +interface UnsignedReports { + projectName: string; + username: string; + week: number; + signed: boolean; +} + +/** + * Renders a component that displays the projects a user is a part of and links to the projects start-page. + * @returns The JSX element representing the component. + */ +function DisplayUserProject(): JSX.Element { + const { projectName } = useParams(); + const [unsignedReports, setUnsignedReports] = useState([]); + //const navigate = useNavigate(); + + // const getUnsignedReports = async (): Promise => { + // const token = localStorage.getItem("accessToken") ?? ""; + // const response = await api.getUserProjects(token); + // console.log(response); + // if (response.success) { + // setUnsignedReports(response.data ?? []); + // } else { + // console.error(response.message); + // } + // }; + + // const handleReportClick = async (projectName: string): Promise => { + // const username = localStorage.getItem("username") ?? ""; + // const token = localStorage.getItem("accessToken") ?? ""; + // const response = await api.checkIfProjectManager( + // username, + // projectName, + // token, + // ); + // if (response.success) { + // if (response.data) { + // navigate(`/PMProjectPage/${projectName}`); + // } else { + // navigate(`/project/${projectName}`); + // } + // } else { + // // handle error + // console.error(response.message); + // } + // }; + + const getUnsignedReports = async (): Promise => { + // Simulate a delay + await Promise.resolve(); + + // Use mock data + const reports: UnsignedReports[] = [ + { + projectName: "projecttest", + username: "user1", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user2", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user3", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user4", + week: 2, + signed: false, + }, + ]; + + // Set the state with the mock data + setUnsignedReports(reports); + }; + + // Call getProjects when the component mounts + useEffect(() => { + void getUnsignedReports(); + }, []); + + return ( + <> +

+ All Unsigned Reports In: {projectName}{" "} +

+
+ {unsignedReports.map( + (unsignedReport: UnsignedReports, index: number) => ( +

+
+
+

{unsignedReport.username}

+ Week: +

{unsignedReport.week}

+ Signed: +

NO

+
+
+
+ +

+ View Report +

+ +
+
+
+

+ ), + )} +
+ + ); +} + +export default DisplayUserProject; diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index 0cd5a8e..92ba84f 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { Project } from "../Types/goTypes"; -import { Link } from "react-router-dom"; +import { useNavigate } from "react-router-dom"; import GetProjects from "./GetProjects"; /** @@ -9,22 +9,52 @@ import GetProjects from "./GetProjects"; */ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); + const navigate = useNavigate(); - GetProjects({ - setProjectsProp: setProjects, - username: localStorage.getItem("username") ?? "", - }); + const getProjects = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUserProjects(token); + console.log(response); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error(response.message); + } + }; + + const handleProjectClick = async (projectName: string): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.checkIfProjectManager(projectName, token); + if (response.success) { + if (response.data) { + navigate(`/PMProjectPage/${projectName}`); + } else { + navigate(`/project/${projectName}`); + } + } else { + // handle error + console.error(response.message); + } + }; + + // Call getProjects when the component mounts + useEffect(() => { + void getProjects(); + }, []); return ( <>

Your Projects

- {projects.map((project, index) => ( - + {projects.map((project) => ( +
void handleProjectClick(project.name)} + key={project.id} + >

{project.name}

- +
))}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index be96329..384359e 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,44 +18,47 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const { projectName } = useParams(); - const { fetchedWeek } = useParams(); - - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - projectName ?? "", - fetchedWeek?.toString() ?? "0", - token, - ); - - if (response.success) { - const report: WeeklyReport = response.data ?? { - reportId: 0, - userId: 0, - projectId: 0, - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; + const { projectName, fetchedWeek } = useParams<{ + projectName: string; + fetchedWeek: string; + }>(); + console.log(projectName, fetchedWeek); useEffect(() => { + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek ?? "", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + void fetchWeeklyReport(); - }); + }, [projectName, fetchedWeek, token]); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { @@ -76,6 +79,7 @@ export default function GetWeeklyReport(): JSX.Element { return ( <> +

Edit Time Report

{ @@ -90,24 +94,10 @@ export default function GetWeeklyReport(): JSX.Element { }} >
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> +
+

Week: {week}

+
+ @@ -127,9 +117,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -146,9 +141,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -165,9 +165,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -184,9 +189,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -203,9 +213,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -222,9 +237,14 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; diff --git a/frontend/src/Components/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index 292ddf5..f684b0c 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -12,66 +12,103 @@ import Button from "./Button"; */ export default function NewWeeklyReport(): JSX.Element { const [week, setWeek] = useState(0); - const [developmentTime, setDevelopmentTime] = useState(); - const [meetingTime, setMeetingTime] = useState(); - const [adminTime, setAdminTime] = useState(); - const [ownWorkTime, setOwnWorkTime] = useState(); - const [studyTime, setStudyTime] = useState(); - const [testingTime, setTestingTime] = useState(); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); const { projectName } = useParams(); const token = localStorage.getItem("accessToken") ?? ""; - const handleNewWeeklyReport = async (): Promise => { + const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { projectName: projectName ?? "", week: week, - developmentTime: developmentTime ?? 0, - meetingTime: meetingTime ?? 0, - adminTime: adminTime ?? 0, - ownWorkTime: ownWorkTime ?? 0, - studyTime: studyTime ?? 0, - testingTime: testingTime ?? 0, + developmentTime: developmentTime, + meetingTime: meetingTime, + adminTime: adminTime, + ownWorkTime: ownWorkTime, + studyTime: studyTime, + testingTime: testingTime, }; - await api.submitWeeklyReport(newWeeklyReport, token); + const response = await api.submitWeeklyReport(newWeeklyReport, token); + console.log(response); + if (response.success) { + return true; + } else { + return false; + } }; const navigate = useNavigate(); + // Check if the browser is Chrome or Edge + const isChromeOrEdge = /Chrome|Edg/.test(navigator.userAgent); return ( <>
{ - if (week === 0) { - alert("Please enter a week number"); - e.preventDefault(); - return; - } e.preventDefault(); - void handleNewWeeklyReport(); - navigate(-1); + void (async (): Promise => { + if (week === 0 || week > 53 || week < 1) { + alert("Please enter a valid week number"); + return; + } + + const success = await handleNewWeeklyReport(); + if (!success) { + alert( + "A Time Report for this week already exists, please go to the edit page to edit it or change week number.", + ); + return; + } + alert("Weekly report submitted successfully"); + navigate(-1); + })(); }} >
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") + {isChromeOrEdge ? ( + { + const weekNumber = parseInt(e.target.value.split("-W")[1]); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> + }} + /> + ) : ( + { + const weekNumber = parseInt(e.target.value); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + )}
@@ -91,9 +128,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime} + value={developmentTime === 0 ? "" : developmentTime} onChange={(e) => { - setDevelopmentTime(parseInt(e.target.value)); + if (e.target.value === "") { + setDevelopmentTime(0); + return; + } else { + setDevelopmentTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -110,9 +152,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime} + value={meetingTime === 0 ? "" : meetingTime} onChange={(e) => { - setMeetingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setMeetingTime(0); + return; + } else { + setMeetingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -129,9 +176,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime} + value={adminTime === 0 ? "" : adminTime} onChange={(e) => { - setAdminTime(parseInt(e.target.value)); + if (e.target.value === "") { + setAdminTime(0); + return; + } else { + setAdminTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -148,9 +200,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime} + value={ownWorkTime === 0 ? "" : ownWorkTime} onChange={(e) => { - setOwnWorkTime(parseInt(e.target.value)); + if (e.target.value === "") { + setOwnWorkTime(0); + return; + } else { + setOwnWorkTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -167,9 +224,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime} + value={studyTime === 0 ? "" : studyTime} onChange={(e) => { - setStudyTime(parseInt(e.target.value)); + if (e.target.value === "") { + setStudyTime(0); + return; + } else { + setStudyTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; @@ -186,9 +248,14 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime} + value={testingTime === 0 ? "" : testingTime} onChange={(e) => { - setTestingTime(parseInt(e.target.value)); + if (e.target.value === "") { + setTestingTime(0); + return; + } else { + setTestingTime(parseInt(e.target.value)); + } }} onKeyDown={(event) => { const keyValue = event.key; diff --git a/frontend/src/Components/OtherUsersTR.tsx b/frontend/src/Components/OtherUsersTR.tsx new file mode 100644 index 0000000..2b00e16 --- /dev/null +++ b/frontend/src/Components/OtherUsersTR.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect } from "react"; +import { WeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ + +//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project. +export default function OtherUsersTR(): JSX.Element { + const [week, setWeek] = useState(0); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); + + const token = localStorage.getItem("accessToken") ?? ""; + const { projectName } = useParams(); + const { username } = useParams(); + const { fetchedWeek } = useParams(); + + useEffect(() => { + const fetchUsersWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchUsersWeeklyReport(); + }); + + return ( + <> +

{username}'s Report

+
+
+
+

Week: {week}

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Activity + Total Time (min) +
Development + +
Meeting + +
Administration + +
Own Work + +
Studies + +
Testing + +
+
+
+ + ); +} diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 73e29e5..60ffcd9 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,91 +1,55 @@ import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; +import { api } from "../API/API"; +import { UserProjectMember } from "../Types/goTypes"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); - const [projectMembers, setProjectMembers] = useState([]); - - // const getProjectMembers = async (): Promise => { - // const token = localStorage.getItem("accessToken") ?? ""; - // const response = await api.getProjectMembers(projectName ?? "", token); - // console.log(response); - // if (response.success) { - // setProjectMembers(response.data ?? []); - // } else { - // console.error(response.message); - // } - // }; - - interface ProjectMember { - username: string; - role: string; - } - - const mockProjectMembers = [ - { - username: "username1", - role: "Project Manager", - }, - { - username: "username2", - role: "System Manager", - }, - { - username: "username3", - role: "Developer", - }, - { - username: "username4", - role: "Tester", - }, - { - username: "username5", - role: "Tester", - }, - { - username: "username6", - role: "Tester", - }, - ]; - - const getProjectMembers = async (): Promise => { - // Use the mock data - setProjectMembers(mockProjectMembers); - - await Promise.resolve(); - }; + const [projectMembers, setProjectMembers] = useState([]); useEffect(() => { + const getProjectMembers = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getAllUsersProject(projectName ?? "", token); + console.log(response); + if (response.success) { + setProjectMembers(response.data ?? []); + } else { + console.error(response.message); + } + }; + void getProjectMembers(); - }); + }, [projectName]); + + interface ProjectMember { + Username: string; + UserRole: string; + } return ( <> +

+ All Members In: {projectName}{" "} +

- {projectMembers.map((projectMember, index) => ( + {projectMembers.map((projectMember: ProjectMember, index: number) => (

-

{projectMember.username}

+

{projectMember.Username}

Role: -

{projectMember.role}

+

{projectMember.UserRole}

View Reports

- -

- Change Role -

-
diff --git a/frontend/src/Components/TimePerActivity.tsx b/frontend/src/Components/TimePerActivity.tsx new file mode 100644 index 0000000..3dc1a6b --- /dev/null +++ b/frontend/src/Components/TimePerActivity.tsx @@ -0,0 +1,175 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for showing total time per role in a project. + * @returns JSX.Element + */ +export default function TimePerRole(): JSX.Element { + const [developmentTime, setDevelopmentTime] = useState(); + const [meetingTime, setMeetingTime] = useState(); + const [adminTime, setAdminTime] = useState(); + const [ownWorkTime, setOwnWorkTime] = useState(); + const [studyTime, setStudyTime] = useState(); + const [testingTime, setTestingTime] = useState(); + + // const token = localStorage.getItem("accessToken") ?? ""; + // const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + + // const fetchTimePerRole = async (): Promise => { + // const response = await api.getTimePerRole( + // username, + // projectName ?? "", + // token, + // ); + // { + // if (response.success) { + // const report: TimePerRole = response.data ?? { + // PManagerTime: 0, + // SManagerTime: 0, + // DeveloperTime: 0, + // TesterTime: 0, + // }; + // } else { + // console.error("Failed to fetch weekly report:", response.message); + // } + // } + + interface TimePerActivity { + developmentTime: number; + meetingTime: number; + adminTime: number; + ownWorkTime: number; + studyTime: number; + testingTime: number; + } + + const fetchTimePerActivity = async (): Promise => { + // Use mock data + const report: TimePerActivity = { + developmentTime: 100, + meetingTime: 200, + adminTime: 300, + ownWorkTime: 50, + studyTime: 75, + testingTime: 110, + }; + + // Set the state with the mock data + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + + await Promise.resolve(); + }; + + useEffect(() => { + void fetchTimePerActivity(); + }); + + return ( + <> +

+ Total Time Per Activity In: {projectName}{" "} +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Activity + Total Time (min) +
Development + { + event.preventDefault(); + }} + /> +
Meeting + { + event.preventDefault(); + }} + /> +
Administration + { + event.preventDefault(); + }} + /> +
Own Work + { + event.preventDefault(); + }} + /> +
Studies + { + event.preventDefault(); + }} + /> +
Testing + { + event.preventDefault(); + }} + /> +
+
+
+ + ); +} diff --git a/frontend/src/Components/TimePerRole.tsx b/frontend/src/Components/TimePerRole.tsx new file mode 100644 index 0000000..f62d83a --- /dev/null +++ b/frontend/src/Components/TimePerRole.tsx @@ -0,0 +1,141 @@ +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; + +/** + * Renders the component for showing total time per role in a project. + * @returns JSX.Element + */ +export default function TimePerRole(): JSX.Element { + const [PManagerTime, setPManagerTime] = useState(0); + const [SManagerTime, setSManagerTime] = useState(0); + const [DeveloperTime, setDeveloperTime] = useState(0); + const [TesterTime, setTesterTime] = useState(0); + + // const token = localStorage.getItem("accessToken") ?? ""; + // const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + + // const fetchTimePerRole = async (): Promise => { + // const response = await api.getTimePerRole( + // username, + // projectName ?? "", + // token, + // ); + // { + // if (response.success) { + // const report: TimePerRole = response.data ?? { + // PManagerTime: 0, + // SManagerTime: 0, + // DeveloperTime: 0, + // TesterTime: 0, + // }; + // } else { + // console.error("Failed to fetch weekly report:", response.message); + // } + // } + + interface TimePerRole { + PManager: number; + SManager: number; + Developer: number; + Tester: number; + } + + const fetchTimePerRole = async (): Promise => { + // Use mock data + const report: TimePerRole = { + PManager: 120, + SManager: 80, + Developer: 200, + Tester: 150, + }; + + // Set the state with the mock data + setPManagerTime(report.PManager); + setSManagerTime(report.SManager); + setDeveloperTime(report.Developer); + setTesterTime(report.Tester); + + await Promise.resolve(); + }; + + useEffect(() => { + void fetchTimePerRole(); + }); + + return ( + <> +

+ Total Time Per Role In: {projectName}{" "} +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Role + Total Time (min) +
Project Manager + { + event.preventDefault(); + }} + /> +
System Manager + { + event.preventDefault(); + }} + /> +
Administration + { + event.preventDefault(); + }} + /> +
Own Work + { + event.preventDefault(); + }} + /> +
+
+
+ + ); +} diff --git a/frontend/src/Components/ViewOtherTimeReport.tsx b/frontend/src/Components/ViewOtherTimeReport.tsx new file mode 100644 index 0000000..32e0716 --- /dev/null +++ b/frontend/src/Components/ViewOtherTimeReport.tsx @@ -0,0 +1,188 @@ +import { useState, useEffect } from "react"; +import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { api } from "../API/API"; +import { useNavigate, useParams } from "react-router-dom"; +import Button from "./Button"; + +/** + * Renders the component for editing a weekly report. + * @returns JSX.Element + */ + +//This component does not yet work as intended. It is supposed to display the weekly report of a user in a project. +export default function GetOtherUsersReport(): JSX.Element { + const [week, setWeek] = useState(0); + const [developmentTime, setDevelopmentTime] = useState(0); + const [meetingTime, setMeetingTime] = useState(0); + const [adminTime, setAdminTime] = useState(0); + const [ownWorkTime, setOwnWorkTime] = useState(0); + const [studyTime, setStudyTime] = useState(0); + const [testingTime, setTestingTime] = useState(0); + + const token = localStorage.getItem("accessToken") ?? ""; + const { projectName } = useParams(); + const { username } = useParams(); + const { fetchedWeek } = useParams(); + + useEffect(() => { + const fetchUsersWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + ); + + if (response.success) { + const report: WeeklyReport = response.data ?? { + reportId: 0, + userId: 0, + projectId: 0, + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; + + void fetchUsersWeeklyReport(); + }); + + const handleSignWeeklyReport = async (): Promise => { + const newWeeklyReport: NewWeeklyReport = { + projectName: projectName ?? "", + week, + developmentTime, + meetingTime, + adminTime, + ownWorkTime, + studyTime, + testingTime, + }; + + await api.submitWeeklyReport(newWeeklyReport, token); + }; + + const navigate = useNavigate(); + + return ( + <> +

{username}'s Report

+
+ { + e.preventDefault(); + void handleSignWeeklyReport(); + navigate(-1); + }} + > +
+
+

Week: {week}

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Activity + + Total Time (min) +
Development + +
Meeting + +
Administration + +
Own Work + +
Studies + +
Testing + +
+
+ +
+ + ); +} diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx new file mode 100644 index 0000000..27092d8 --- /dev/null +++ b/frontend/src/Containers/GenApiDemo.tsx @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { GenApi } from "../API/GenApi"; + +// Instantiation of the API +const api = new GenApi(); + +export function GenApiDemo(): JSX.Element { + // Sync wrapper around the loginCreate method + const register = async (): Promise => { + const response = await api.login.loginCreate({ + username: "admin", + password: "admin", + }); + console.log(response.data); // This should be the inner type of the response + }; + + // Call the wrapper + useEffect(() => { + void register(); + }); + + return <>; +} diff --git a/frontend/src/Pages/AdminPages/AdminProjectPage.tsx b/frontend/src/Pages/AdminPages/AdminProjectPage.tsx index 0faae7e..db51319 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectPage.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectPage.tsx @@ -1,17 +1,26 @@ +import { useParams } from "react-router-dom"; +import { api } from "../../API/API"; import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; +async function handleDeleteProject( + projectName: string, + token: string, +): Promise { + await api.removeProject(projectName, token); +} + function AdminProjectPage(): JSX.Element { const content = <>; + const { projectName } = useParams(); + const token = localStorage.getItem("accessToken"); const buttons = ( <>