diff --git a/.gitignore b/.gitignore index 281e866..05f913b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,10 +15,6 @@ 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 90fabf6..cb905e4 100644 --- a/Justfile +++ b/Justfile @@ -23,13 +23,10 @@ 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 51fb206..97db62e 100644 --- a/Makefile +++ b/Makefile @@ -13,13 +13,10 @@ 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 0ffc557..331f8d5 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -8,19 +8,17 @@ 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/$(PROC_NAME) main.go + $(GOBUILD) -o bin/server main.go # Run target run: build - ./bin/$(PROC_NAME) + ./bin/server watch: build watchexec -c -w . -r make run @@ -34,22 +32,11 @@ 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 ./... @@ -105,17 +92,6 @@ 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 7a08b0e..322c812 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "Logs in a user and returns a JWT token", + "description": "logs the user in and returns a jwt token", "consumes": [ "application/json" ], "produces": [ - "application/json" + "text/plain" ], "tags": [ - "Auth" + "User" ], - "summary": "Login", + "summary": "login", "parameters": [ { - "description": "User credentials", - "name": "body", + "description": "login info", + "name": "NewUser", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "JWT token", + "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "400": { @@ -71,26 +71,29 @@ const docTemplate = `{ } } }, - "/loginrenew": { + "/loginerenew": { "post": { "security": [ { - "JWT": [] + "bererToken": [] } ], - "description": "Renews the users token.", - "produces": [ + "description": "renews the users token", + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ - "Auth" + "User" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "401": { @@ -110,12 +113,7 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "security": [ - { - "JWT": [] - } - ], - "description": "Promote chosen user to site admin", + "description": "promote chosen user to admin", "consumes": [ "application/json" ], @@ -139,13 +137,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully promoted user", + "description": "Successfully prometed user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "json" } }, "400": { - "description": "Bad request", + "description": "bad request", "schema": { "type": "string" } @@ -175,7 +173,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "Auth" + "User" ], "summary": "Register", "parameters": [ @@ -213,11 +211,6 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { - "security": [ - { - "JWT": [] - } - ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -259,27 +252,22 @@ const docTemplate = `{ }, "/users/all": { "get": { - "security": [ - { - "JWT": [] - } - ], "description": "lists all users", - "produces": [ + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ "User" ], "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully returned all users", + "description": "Successfully signed token for user", "schema": { - "type": "array", - "items": { - "type": "string" - } + "type": "json" } }, "401": { @@ -303,27 +291,16 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string", - "example": "password123" + "type": "string" }, "username": { - "type": "string", - "example": "username123" - } - } - }, - "types.Token": { - "type": "object", - "properties": { - "token": { "type": "string" } } } }, "securityDefinitions": { - "JWT": { - "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", + "bererToken": { "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index f4c0f6e..fd0a083 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -36,13 +36,10 @@ 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 + GetTotalTimePerActivity(projectName string) (map[string]int, error) } // This struct is a wrapper type that holds the database connection @@ -356,51 +353,6 @@ 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 @@ -500,26 +452,6 @@ 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 @@ -559,11 +491,11 @@ func (d *Db) MigrateSampleData() error { return nil } -// GetProjectTimes retrieves a map with times per "Activity" for a given project -func (d *Db) GetProjectTimes(projectName string) (map[string]int, error) { +func (d *Db) GetTotalTimePerActivity(projectName string) (map[string]int, error) { + query := ` SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time - FROM weekly_reports + FROM weekly_reports JOIN projects ON weekly_reports.project_id = projects.id WHERE projects.name = ? ` @@ -596,8 +528,3 @@ 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 fe3e6cd..139fba9 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -470,47 +470,6 @@ 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() @@ -770,90 +729,27 @@ func TestIsProjectManager(t *testing.T) { } } -func TestGetProjectTimes(t *testing.T) { - // Initialize +func TestGetTotalTimePerActivity(t *testing.T) { + // Initialize your test database connection 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 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) + // Run the query to get total time per activity + totalTime, err := db.GetTotalTimePerActivity("projecttest") 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 } - // 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]) - } - } - - // 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 - } - - newExpectedTotalTime := map[string]int{ - "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]) - } - } + // ska lägga till fler assertions } + func TestEnsureManagerOfCreatedProject(t *testing.T) { db, err := setupState() if err != nil { @@ -887,81 +783,3 @@ 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 deleted file mode 100644 index 69fa3a2..0000000 --- a/backend/internal/database/middleware.go +++ /dev/null @@ -1,17 +0,0 @@ -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/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index ab74f1a..092fbb0 100644 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ b/backend/internal/database/sample_data/0010_sample_data.sql @@ -7,8 +7,6 @@ VALUES ("user", "123"); INSERT OR IGNORE INTO users(username, password) VALUES ("user2", "123"); -INSERT OR IGNORE INTO site_admin VALUES (1); - INSERT OR IGNORE INTO projects(name,description,owner_user_id) VALUES ("projecttest","test project", 1); diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go new file mode 100644 index 0000000..b88bdcd --- /dev/null +++ b/backend/internal/handlers/global_state.go @@ -0,0 +1,41 @@ +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 new file mode 100644 index 0000000..c0b64f7 --- /dev/null +++ b/backend/internal/handlers/global_state_test.go @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..603f4cd --- /dev/null +++ b/backend/internal/handlers/handlers_project_related.go @@ -0,0 +1,235 @@ +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 { + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Then dip into the database to get the projects + projects, err := gs.Db.GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} + +// ProjectRoleChange is a handler that changes a user's role within a project +func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { + + //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 new file mode 100644 index 0000000..47d076d --- /dev/null +++ b/backend/internal/handlers/handlers_report_related.go @@ -0,0 +1,143 @@ +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") + 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 new file mode 100644 index 0000000..116ce90 --- /dev/null +++ b/backend/internal/handlers/handlers_user_related.go @@ -0,0 +1,269 @@ +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 another user") + return c.Status(403).SendString("You can only delete yourself") + } + + if err := gs.Db.RemoveUser(username); err != nil { + log.Warn("Error deleting user:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("User deleted:", username) + return c.Status(200).SendString("User deleted") +} + +// 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 prometed 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) +} + +// Changes a users name 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) + projectManagerUsername := claims["name"].(string) + log.Info(projectManagerUsername) + // Extract the necessary parameters from the request + data := new(types.NameChange) + if err := c.BodyParser(data); err != nil { + log.Info("error parsing username, project or role") + return c.Status(400).SendString(err.Error()) + } + + // dubble diping and checcking if current user is + + if ismanager, err := gs.Db.IsProjectManager(projectManagerUsername, c.Params(data.Name)); err != nil { + log.Warn("Error checking if projectmanager:", err) + return c.Status(500).SendString(err.Error()) + } else if !ismanager { + log.Warn("tried changing name when not projectmanager:", err) + return c.Status(401).SendString("you can not change name when not projectmanager") + } + + // Change the user's name within the project in the database + if err := gs.Db.ChangeUserName(projectManagerUsername, data.Name); 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 deleted file mode 100644 index 702b7dd..0000000 --- a/backend/internal/handlers/projects/AddUserToProject.go +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index cef2f2b..0000000 --- a/backend/internal/handlers/projects/CreateProject.go +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 415424a..0000000 --- a/backend/internal/handlers/projects/DeleteProject.go +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index 03333ce..0000000 --- a/backend/internal/handlers/projects/GetProject.go +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 573a95e..0000000 --- a/backend/internal/handlers/projects/GetProjectTimes.go +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 99ed63b..0000000 --- a/backend/internal/handlers/projects/GetUserProject.go +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 678fad5..0000000 --- a/backend/internal/handlers/projects/IsProjectManager.go +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index e0bcaf5..0000000 --- a/backend/internal/handlers/projects/ListAllUserProjects.go +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 266127d..0000000 --- a/backend/internal/handlers/projects/ProjectRoleChange.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 7b140dd..0000000 --- a/backend/internal/handlers/projects/RemoveProject.go +++ /dev/null @@ -1,35 +0,0 @@ -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 deleted file mode 100644 index 9525f55..0000000 --- a/backend/internal/handlers/reports/GetUnsignedReports.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index 422bc0b..0000000 --- a/backend/internal/handlers/reports/GetWeeklyReport.go +++ /dev/null @@ -1,47 +0,0 @@ -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 deleted file mode 100644 index da8a90b..0000000 --- a/backend/internal/handlers/reports/GetWeeklyReportsUserHandler.go +++ /dev/null @@ -1,36 +0,0 @@ -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 deleted file mode 100644 index a486ecc..0000000 --- a/backend/internal/handlers/reports/SignReport.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 900aa03..0000000 --- a/backend/internal/handlers/reports/SubmitWeeklyReport.go +++ /dev/null @@ -1,41 +0,0 @@ -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 deleted file mode 100644 index 3ab835d..0000000 --- a/backend/internal/handlers/reports/UpdateWeeklyReport.go +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 75032e4..0000000 --- a/backend/internal/handlers/users/ChangeUserName.go +++ /dev/null @@ -1,44 +0,0 @@ -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 deleted file mode 100644 index 10a6ec6..0000000 --- a/backend/internal/handlers/users/GetUsersProjects.go +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 5ac5df0..0000000 --- a/backend/internal/handlers/users/ListAllUsers.go +++ /dev/null @@ -1,32 +0,0 @@ -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 deleted file mode 100644 index 42c52a5..0000000 --- a/backend/internal/handlers/users/Login.go +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index 3926ce4..0000000 --- a/backend/internal/handlers/users/LoginRenew.go +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 3f0a6d3..0000000 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ /dev/null @@ -1,45 +0,0 @@ -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 deleted file mode 100644 index b9e0c78..0000000 --- a/backend/internal/handlers/users/Register.go +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 491a1b3..0000000 --- a/backend/internal/handlers/users/UserDelete.go +++ /dev/null @@ -1,43 +0,0 @@ -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 234781b..8d22b6a 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -65,24 +65,3 @@ 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 37cc8c2..88b4f06 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" example:"username123"` - Password string `json:"password" example:"password123"` + Username string `json:"username"` + Password string `json:"password"` } // 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 cf58280..ff6b94e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -6,9 +6,7 @@ import ( _ "ttime/docs" "ttime/internal/config" "ttime/internal/database" - "ttime/internal/handlers/projects" - "ttime/internal/handlers/reports" - "ttime/internal/handlers/users" + "ttime/internal/handlers" "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" @@ -25,22 +23,15 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -// @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 ".** +//@securityDefinitions.apikey bererToken +//@in header +//@name Authorization // @host localhost:8080 // @BasePath /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. -*/ +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ func main() { conf, err := config.ReadConfigFromFile("config.toml") @@ -57,28 +48,24 @@ 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) @@ -86,50 +73,34 @@ func main() { // This will likely be replaced by an embedded filesystem in the future server.Static("/", "./static") - // Create a group for our API - api := server.Group("/api") - // Register our unprotected routes - api.Post("/register", users.Register) - api.Post("/login", users.Login) + server.Post("/api/register", gs.Register) + server.Post("/api/login", gs.Login) - // Every route from here on will require a valid - // JWT bearer token authentication in the header + // Every route from here on will require a valid JWT server.Use(jwtware.New(jwtware.Config{ SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) - // 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) + // Protected routes (require a valid JWT bearer token authentication header) + server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport) + server.Get("/api/getUserProjects", gs.GetUserProjects) + server.Post("/api/loginrenew", gs.LoginRenew) + server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches + server.Delete("api/project/:projectID", gs.DeleteProject) // WIP + server.Post("/api/project", gs.CreateProject) // WIP + server.Get("/api/project/:projectId", gs.GetProject) + server.Get("/api/project/getAllUsers", gs.GetAllUsersProject) + server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) + server.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) // 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 f9cb39d..ecd2f84 100644 --- a/container/Containerfile +++ b/container/Containerfile @@ -13,6 +13,7 @@ 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 ./ @@ -23,7 +24,9 @@ RUN go mod download # Add the source code ADD backend . -RUN go build -o server +RUN make migrate + +# 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 @@ -34,7 +37,6 @@ 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 @@ -42,6 +44,9 @@ 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 deleted file mode 100644 index c49d006..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -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 886c957..0859748 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -4,10 +4,7 @@ import { User, Project, NewProject, - UserProjectMember, WeeklyReport, - StrNameChange, - NewProjMember, } from "../Types/goTypes"; /** @@ -49,6 +46,7 @@ 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>; @@ -86,7 +84,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. @@ -129,42 +127,6 @@ interface API { * @returns {Promise>} A promise resolving to an API response containing the list of users. */ getAllUsers(token: string): Promise>; - /** Gets all users in a project from name*/ - getAllUsersProject( - projectName: string, - token: string, - ): Promise>; - /** - * Changes the username of a user in the database. - * @param {StrNameChange} data The object containing the previous and new username. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response. - */ - changeUserName( - data: StrNameChange, - token: string, - ): Promise>; - addUserToProject( - 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 */ @@ -202,17 +164,19 @@ export const api: API = { ): Promise> { try { const response = await fetch(`/api/userdelete/${username}`, { - method: "DELETE", + method: "POST", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, body: JSON.stringify(username), }); + if (!response.ok) { - return { success: false, message: "Could not remove user" }; + return { success: false, message: "Failed to remove user" }; } else { - return { success: true }; + const data = (await response.json()) as User; + return { success: true, data }; } } catch (e) { return { success: false, message: "Failed to remove user" }; @@ -220,20 +184,19 @@ export const api: API = { }, async checkIfProjectManager( + username: string, projectName: string, token: string, ): Promise> { try { - const response = await fetch( - `/api/checkIfProjectManager/${projectName}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, + const response = await fetch("/api/checkIfProjectManager", { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, }, - ); + body: JSON.stringify({ username, projectName }), + }); if (!response.ok) { return { @@ -245,7 +208,7 @@ export const api: API = { return { success: true, data }; } } catch (e) { - return { success: false, message: "Failed to check if project manager" }; + return { success: false, message: "fuck" }; } }, @@ -274,30 +237,6 @@ export const api: API = { } }, - async addUserToProject( - user: NewProjMember, - token: string, - ): Promise> { - try { - const response = await fetch("/api/addUserToProject", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(user), - }); - - if (!response.ok) { - return { success: false, message: "Failed to add member" }; - } else { - return { success: true, message: "Added member" }; - } - } catch (e) { - return { success: false, message: "Failed to add member" }; - } - }, - async renewToken(token: string): Promise> { try { const response = await fetch("/api/loginrenew", { @@ -349,7 +288,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -367,8 +306,8 @@ export const api: API = { }; } - const data = await response.text(); - return { success: true, message: data }; + const data = (await response.json()) as NewWeeklyReport; + return { success: true, data }; } catch (e) { return { success: false, @@ -509,111 +448,4 @@ export const api: API = { }); } }, - //Gets all users in a project - async getAllUsersProject( - projectName: string, - token: string, - ): Promise> { - try { - const response = await fetch(`/api/getUsersProject/${projectName}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }); - - if (!response.ok) { - return Promise.resolve({ - success: false, - message: "Failed to get users", - }); - } else { - const data = (await response.json()) as UserProjectMember[]; - return Promise.resolve({ success: true, data }); - } - } catch (e) { - return Promise.resolve({ - success: false, - message: "API is not ok", - }); - } - }, - - async changeUserName( - data: StrNameChange, - token: string, - ): Promise> { - try { - const response = await fetch("/api/changeUserName", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(data), - }); - - if (!response.ok) { - return { success: false, message: "Failed to change username" }; - } else { - return { success: true }; - } - } catch (e) { - 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 deleted file mode 100644 index 8ca851b..0000000 --- a/frontend/src/API/GenApi.ts +++ /dev/null @@ -1,358 +0,0 @@ -/* 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/AddMember.tsx b/frontend/src/Components/AddMember.tsx deleted file mode 100644 index d29be68..0000000 --- a/frontend/src/Components/AddMember.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { APIResponse, api } from "../API/API"; -import { NewProjMember } from "../Types/goTypes"; - -/** - * Tries to add a member to a project - * @param {Object} props - A NewProjMember - * @returns {boolean} True if added, false if not - */ -function AddMember(props: { memberToAdd: NewProjMember }): boolean { - let added = false; - if ( - props.memberToAdd.username === "" || - props.memberToAdd.role === "" || - props.memberToAdd.projectname === "" - ) { - alert("All fields must be filled before adding"); - return added; - } - api - .addUserToProject( - props.memberToAdd, - localStorage.getItem("accessToken") ?? "", - ) - .then((response: APIResponse) => { - if (response.success) { - alert("Member added"); - added = true; - } else { - alert("Member not added"); - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during member add:", error); - }); - return added; -} - -export default AddMember; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index 839cfa7..f5f4a08 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -37,7 +37,7 @@ function CreateProject(props: { name: string; description: string }): boolean { * Provides UI for adding a project to the system. * @returns {JSX.Element} - Returns the component UI for adding a project */ -function AddProject() { +function AddProject(): JSX.Element { const [name, setName] = useState(""); const [description, setDescription] = useState(""); @@ -45,7 +45,6 @@ function AddProject() {
{ e.preventDefault(); @@ -79,6 +78,9 @@ function AddProject() {
diff --git a/frontend/src/Components/AddUserToProject.tsx b/frontend/src/Components/AddUserToProject.tsx deleted file mode 100644 index 9f4439b..0000000 --- a/frontend/src/Components/AddUserToProject.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { useState } from "react"; -import { NewProjMember } from "../Types/goTypes"; -import Button from "./Button"; -import GetAllUsers from "./GetAllUsers"; -import AddMember from "./AddMember"; -import BackButton from "./BackButton"; - -/** - * Provides UI for adding a member to a project. - * @returns {JSX.Element} - Returns the component UI for adding a member - */ -function AddUserToProject(): JSX.Element { - const [name, setName] = useState(""); - const [users, setUsers] = useState([]); - const [role, setRole] = useState(""); - GetAllUsers({ setUsersProp: setUsers }); - - const handleClick = (): boolean => { - const newMember: NewProjMember = { - username: name, - projectname: localStorage.getItem("projectName") ?? "", - role: role, - }; - return AddMember({ memberToAdd: newMember }); - }; - - return ( -
-

- User chosen: [{name}] -

-

- Role chosen: [{role}] -

-

- Project chosen: [{localStorage.getItem("projectName") ?? ""}] -

-

Choose role:

-
-
    -
  • { - setRole("member"); - }} - > - {"Member"} -
  • -
  • { - setRole("project_manager"); - }} - > - {"Project manager"} -
  • -
-
-

Choose user:

-
-
    -
    - {users.map((user) => ( -
  • { - setName(user); - }} - > - {user} -
  • - ))} -
-
-
-
-

-
- ); -} - -export default AddUserToProject; diff --git a/frontend/src/Components/AdminUserList.tsx b/frontend/src/Components/AdminUserList.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx index 4fa9ad8..14d3af9 100644 --- a/frontend/src/Components/AllTimeReportsInProject.tsx +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -13,24 +13,24 @@ function AllTimeReportsInProject(): JSX.Element { const { projectName } = useParams(); const [weeklyReports, setWeeklyReports] = useState([]); + 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); + } + }; + // 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); - } - }; - void getWeeklyReports(); - }, [projectName]); + }, []); return ( <> diff --git a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx b/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx deleted file mode 100644 index 09ca6dc..0000000 --- a/frontend/src/Components/AllTimeReportsInProjectOtherUser.tsx +++ /dev/null @@ -1,103 +0,0 @@ -//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/Button.tsx b/frontend/src/Components/Button.tsx index aa38c9e..13ae807 100644 --- a/frontend/src/Components/Button.tsx +++ b/frontend/src/Components/Button.tsx @@ -13,7 +13,7 @@ function Button({ type, }: { text: string; - onClick?: () => void; + onClick: () => void; type: "submit" | "button" | "reset"; }): JSX.Element { return ( diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index e297a04..3c35e94 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,48 +1,23 @@ import React, { useState } from "react"; import InputField from "./InputField"; -import { api } from "../API/API"; function ChangeUsername(): JSX.Element { const [newUsername, setNewUsername] = useState(""); - const [errorMessage, setErrorMessage] = useState(""); const handleChange = (e: React.ChangeEvent): void => { setNewUsername(e.target.value); }; - const handleSubmit = async (): Promise => { - try { - // Call the API function to change the username - const token = localStorage.getItem("accessToken"); - if (!token) { - throw new Error("Access token not found"); - } - - const response = await api.changeUserName( - { prevName: "currentName", newName: newUsername }, - token, - ); - - if (response.success) { - // Optionally, add a success message or redirect the user - console.log("Username changed successfully"); - } else { - // Handle the error message - console.error("Failed to change username:", response.message); - setErrorMessage(response.message ?? "Failed to change username"); - } - } catch (error) { - console.error("Error changing username:", error); - // Optionally, handle the error - setErrorMessage("Failed to change username"); - } - }; - - const handleButtonClick = (): void => { - handleSubmit().catch((error) => { - console.error("Error in handleSubmit:", error); - }); - }; + // const handleSubmit = async (): Promise => { + // try { + // // Call the API function to update the username + // await api.updateUsername(newUsername); + // // Optionally, add a success message or redirect the user + // } catch (error) { + // console.error("Error updating username:", error); + // // Optionally, handle the error + // } + // }; return (
@@ -52,8 +27,6 @@ function ChangeUsername(): JSX.Element { value={newUsername} onChange={handleChange} /> - {errorMessage &&
{errorMessage}
} -
); } diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx index d1dbc7f..db49724 100644 --- a/frontend/src/Components/DeleteUser.tsx +++ b/frontend/src/Components/DeleteUser.tsx @@ -11,6 +11,7 @@ import { api, APIResponse } from "../API/API"; */ function DeleteUser(props: { usernameToDelete: string }): boolean { + //console.log(props.usernameToDelete); FOR DEBUG let removed = false; api .removeUser( @@ -19,16 +20,12 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { ) .then((response: APIResponse) => { if (response.success) { - alert("User has been deleted!"); - location.reload(); removed = true; } else { - alert("User has not been deleted"); console.error(response.message); } }) .catch((error) => { - alert("User has not been deleted"); console.error("An error occurred during creation:", error); }); return removed; diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx deleted file mode 100644 index 780f20c..0000000 --- a/frontend/src/Components/DisplayUnsignedReports.tsx +++ /dev/null @@ -1,129 +0,0 @@ -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 29e4bcb..f4fd782 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { Project } from "../Types/goTypes"; -import { useNavigate } from "react-router-dom"; +import { Link } from "react-router-dom"; import { api } from "../API/API"; /** @@ -9,7 +9,6 @@ import { api } from "../API/API"; */ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); - const navigate = useNavigate(); const getProjects = async (): Promise => { const token = localStorage.getItem("accessToken") ?? ""; @@ -22,21 +21,6 @@ function DisplayUserProject(): JSX.Element { } }; - 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(); @@ -46,15 +30,12 @@ function DisplayUserProject(): JSX.Element { <>

Your Projects

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

{project.name}

-
+ ))}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 384359e..76c2b94 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -18,47 +18,46 @@ export default function GetWeeklyReport(): JSX.Element { const [testingTime, setTestingTime] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; - const { projectName, fetchedWeek } = useParams<{ - projectName: string; - fetchedWeek: string; - }>(); - console.log(projectName, fetchedWeek); + const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + const { fetchedWeek } = useParams(); + + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + username, + 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); + } + }; 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 = { @@ -79,7 +78,6 @@ export default function GetWeeklyReport(): JSX.Element { return ( <> -

Edit Time Report

{ @@ -94,10 +92,24 @@ export default function GetWeeklyReport(): JSX.Element { }} >
-
-

Week: {week}

-
- + { + const weekNumber = parseInt(e.target.value.split("-W")[1]); + setWeek(weekNumber); + }} + onKeyDown={(event) => { + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> @@ -117,14 +129,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime === 0 ? "" : developmentTime} + value={developmentTime} onChange={(e) => { - if (e.target.value === "") { - setDevelopmentTime(0); - return; - } else { - setDevelopmentTime(parseInt(e.target.value)); - } + setDevelopmentTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -141,14 +148,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime === 0 ? "" : meetingTime} + value={meetingTime} onChange={(e) => { - if (e.target.value === "") { - setMeetingTime(0); - return; - } else { - setMeetingTime(parseInt(e.target.value)); - } + setMeetingTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -165,14 +167,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime === 0 ? "" : adminTime} + value={adminTime} onChange={(e) => { - if (e.target.value === "") { - setAdminTime(0); - return; - } else { - setAdminTime(parseInt(e.target.value)); - } + setAdminTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -189,14 +186,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime === 0 ? "" : ownWorkTime} + value={ownWorkTime} onChange={(e) => { - if (e.target.value === "") { - setOwnWorkTime(0); - return; - } else { - setOwnWorkTime(parseInt(e.target.value)); - } + setOwnWorkTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -213,14 +205,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime === 0 ? "" : studyTime} + value={studyTime} onChange={(e) => { - if (e.target.value === "") { - setStudyTime(0); - return; - } else { - setStudyTime(parseInt(e.target.value)); - } + setStudyTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -237,14 +224,9 @@ export default function GetWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime === 0 ? "" : testingTime} + value={testingTime} onChange={(e) => { - if (e.target.value === "") { - setTestingTime(0); - return; - } else { - setTestingTime(parseInt(e.target.value)); - } + setTestingTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; diff --git a/frontend/src/Components/GetProjects.tsx b/frontend/src/Components/GetProjects.tsx deleted file mode 100644 index d6ab1f7..0000000 --- a/frontend/src/Components/GetProjects.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Dispatch, useEffect } from "react"; -import { Project } from "../Types/goTypes"; -import { api } from "../API/API"; - -/** - * Gets all projects that user is a member of - * @param props - A setStateAction for the array you want to put projects in - * @returns {void} Nothing - * @example - * const [projects, setProjects] = useState([]); - * GetAllUsers({ setProjectsProp: setProjects }); - */ -function GetProjects(props: { - setProjectsProp: Dispatch>; -}): void { - const setProjects: Dispatch> = - props.setProjectsProp; - useEffect(() => { - const fetchUsers = async (): Promise => { - try { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(token); - if (response.success) { - setProjects(response.data ?? []); - } else { - console.error("Failed to fetch projects:", response.message); - } - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - - void fetchUsers(); - }, [setProjects]); -} - -export default GetProjects; diff --git a/frontend/src/Components/GetUsersInProject.tsx b/frontend/src/Components/GetUsersInProject.tsx deleted file mode 100644 index acdd965..0000000 --- a/frontend/src/Components/GetUsersInProject.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Dispatch, useEffect } from "react"; -import { UserProjectMember } from "../Types/goTypes"; -import { api } from "../API/API"; - -/** - * Gets all projects that user is a member of - * @param props - A setStateAction for the array you want to put projects in - * @returns {void} Nothing - * @example - * const [projects, setProjects] = useState([]); - * GetAllUsers({ setProjectsProp: setProjects }); - */ -function GetUsersInProject(props: { - projectName: string; - setUsersProp: Dispatch>; -}): void { - const setUsers: Dispatch> = - props.setUsersProp; - useEffect(() => { - const fetchUsers = async (): Promise => { - try { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getAllUsersProject(props.projectName, token); - if (response.success) { - setUsers(response.data ?? []); - } else { - console.error("Failed to fetch projects:", response.message); - } - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - void fetchUsers(); - }, [props.projectName, setUsers]); -} - -export default GetUsersInProject; diff --git a/frontend/src/Components/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index f684b0c..292ddf5 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -12,103 +12,66 @@ import Button from "./Button"; */ export default function NewWeeklyReport(): 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 [developmentTime, setDevelopmentTime] = useState(); + const [meetingTime, setMeetingTime] = useState(); + const [adminTime, setAdminTime] = useState(); + const [ownWorkTime, setOwnWorkTime] = useState(); + const [studyTime, setStudyTime] = useState(); + const [testingTime, setTestingTime] = useState(); 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, - meetingTime: meetingTime, - adminTime: adminTime, - ownWorkTime: ownWorkTime, - studyTime: studyTime, - testingTime: testingTime, + developmentTime: developmentTime ?? 0, + meetingTime: meetingTime ?? 0, + adminTime: adminTime ?? 0, + ownWorkTime: ownWorkTime ?? 0, + studyTime: studyTime ?? 0, + testingTime: testingTime ?? 0, }; - const response = await api.submitWeeklyReport(newWeeklyReport, token); - console.log(response); - if (response.success) { - return true; - } else { - return false; - } + await api.submitWeeklyReport(newWeeklyReport, token); }; 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 (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); - })(); + void handleNewWeeklyReport(); + navigate(-1); }} >
- {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) => { + { + 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(); - }} - /> - ) : ( - { - 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(); - }} - /> - )} + }} + onPaste={(event) => { + event.preventDefault(); + }} + />
@@ -128,14 +91,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime === 0 ? "" : developmentTime} + value={developmentTime} onChange={(e) => { - if (e.target.value === "") { - setDevelopmentTime(0); - return; - } else { - setDevelopmentTime(parseInt(e.target.value)); - } + setDevelopmentTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -152,14 +110,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime === 0 ? "" : meetingTime} + value={meetingTime} onChange={(e) => { - if (e.target.value === "") { - setMeetingTime(0); - return; - } else { - setMeetingTime(parseInt(e.target.value)); - } + setMeetingTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -176,14 +129,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime === 0 ? "" : adminTime} + value={adminTime} onChange={(e) => { - if (e.target.value === "") { - setAdminTime(0); - return; - } else { - setAdminTime(parseInt(e.target.value)); - } + setAdminTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -200,14 +148,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime === 0 ? "" : ownWorkTime} + value={ownWorkTime} onChange={(e) => { - if (e.target.value === "") { - setOwnWorkTime(0); - return; - } else { - setOwnWorkTime(parseInt(e.target.value)); - } + setOwnWorkTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -224,14 +167,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime === 0 ? "" : studyTime} + value={studyTime} onChange={(e) => { - if (e.target.value === "") { - setStudyTime(0); - return; - } else { - setStudyTime(parseInt(e.target.value)); - } + setStudyTime(parseInt(e.target.value)); }} onKeyDown={(event) => { const keyValue = event.key; @@ -248,14 +186,9 @@ export default function NewWeeklyReport(): JSX.Element { type="number" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime === 0 ? "" : testingTime} + value={testingTime} onChange={(e) => { - if (e.target.value === "") { - setTestingTime(0); - return; - } else { - setTestingTime(parseInt(e.target.value)); - } + 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 deleted file mode 100644 index 2b00e16..0000000 --- a/frontend/src/Components/OtherUsersTR.tsx +++ /dev/null @@ -1,153 +0,0 @@ -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/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx deleted file mode 100644 index 3075b19..0000000 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { useState } from "react"; -import Button from "./Button"; -import { UserProjectMember } from "../Types/goTypes"; -import GetUsersInProject from "./GetUsersInProject"; -import { Link } from "react-router-dom"; - -function ProjectInfoModal(props: { - isVisible: boolean; - projectname: string; - onClose: () => void; - onClick: (username: string) => void; -}): JSX.Element { - const [users, setUsers] = useState([]); - GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); - if (!props.isVisible) return <>; - - return ( -
-
-
-

- {localStorage.getItem("projectName") ?? ""} -

-

Project members:

-
-
    -
    - {users.map((user) => ( -
  • { - props.onClick(user.Username); - }} - > - - Name: {user.Username} -
    - Role: {user.UserRole} -
    -
  • - ))} -
-
-
-
-
-
-
- ); -} - -export default ProjectInfoModal; diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx deleted file mode 100644 index f25ee47..0000000 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { useState } from "react"; -import { NewProject } from "../Types/goTypes"; -import ProjectInfoModal from "./ProjectInfoModal"; -import UserInfoModal from "./UserInfoModal"; - -/** - * A list of projects for admin manage projects page, that sets an onClick - * function for eact project
  • element, which displays a modul with - * user info. - * @param props - An array of projects to display - * @returns {JSX.Element} The project list - * @example - * const projects: NewProject[] = [{ name: "Project", description: "New" }]; - * return - */ - -export function ProjectListAdmin(props: { - projects: NewProject[]; -}): JSX.Element { - const [projectModalVisible, setProjectModalVisible] = useState(false); - const [projectname, setProjectname] = useState(""); - const [userModalVisible, setUserModalVisible] = useState(false); - const [username, setUsername] = useState(""); - - const handleClickUser = (username: string): void => { - setUsername(username); - setUserModalVisible(true); - }; - - const handleClickProject = (projectname: string): void => { - setProjectname(projectname); - localStorage.setItem("projectName", projectname); - setProjectModalVisible(true); - }; - - const handleCloseProject = (): void => { - setProjectname(""); - setProjectModalVisible(false); - }; - - const handleCloseUser = (): void => { - setProjectname(""); - setUserModalVisible(false); - }; - - return ( - <> - - { - return; - }} - isVisible={userModalVisible} - username={username} - /> -
    -
      - {props.projects.map((project) => ( -
    • { - handleClickProject(project.name); - }} - > - {project.name} -
    • - ))} -
    -
    - - ); -} diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 60ffcd9..73e29e5 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,55 +1,91 @@ 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 [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]); + // 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; - UserRole: string; + 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(); + }; + + useEffect(() => { + void getProjectMembers(); + }); + return ( <> -

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

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

    -

    {projectMember.Username}

    +

    {projectMember.username}

    Role: -

    {projectMember.UserRole}

    +

    {projectMember.role}

    View Reports

    + +

    + Change Role +

    +
    diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 6192637..df07c6e 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -4,6 +4,7 @@ import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; import InputField from "./InputField"; +import { useNavigate } from "react-router-dom"; /** * Renders a registration form for the admin to add new users in. @@ -14,6 +15,8 @@ export default function Register(): JSX.Element { const [password, setPassword] = useState(); const [errMessage, setErrMessage] = useState(); + const nav = useNavigate(); + const handleRegister = async (): Promise => { const newUser: NewUser = { username: username ?? "", @@ -21,9 +24,8 @@ export default function Register(): JSX.Element { }; const response = await api.registerUser(newUser); if (response.success) { - alert("User added!"); + nav("/"); // Instantly navigate to the login page } else { - alert("User not added"); setErrMessage(response.message ?? "Unknown error"); console.error(errMessage); } diff --git a/frontend/src/Components/TimePerActivity.tsx b/frontend/src/Components/TimePerActivity.tsx deleted file mode 100644 index 3dc1a6b..0000000 --- a/frontend/src/Components/TimePerActivity.tsx +++ /dev/null @@ -1,175 +0,0 @@ -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 deleted file mode 100644 index f62d83a..0000000 --- a/frontend/src/Components/TimePerRole.tsx +++ /dev/null @@ -1,141 +0,0 @@ -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/UserInfoModal.tsx b/frontend/src/Components/UserInfoModal.tsx index 9695899..a22ef01 100644 --- a/frontend/src/Components/UserInfoModal.tsx +++ b/frontend/src/Components/UserInfoModal.tsx @@ -5,38 +5,23 @@ import UserProjectListAdmin from "./UserProjectListAdmin"; function UserInfoModal(props: { isVisible: boolean; - manageMember: boolean; username: string; onClose: () => void; - onDelete: (username: string) => void; }): JSX.Element { if (!props.isVisible) return <>; - const ManageUserOrMember = (check: boolean): JSX.Element => { - if (check) { - return ( - -

    - (Change Role) -

    - - ); - } - return ( - -

    - (Change Username) -

    - - ); - }; + return (
    -
    +

    {props.username}

    - {ManageUserOrMember(props.manageMember)} + +

    + (Change Username) +

    +

    Member of these projects: @@ -49,13 +34,7 @@ function UserInfoModal(props: {

    - -
    - - ); -} diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx deleted file mode 100644 index 27092d8..0000000 --- a/frontend/src/Containers/GenApiDemo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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/AdminManageProjects.tsx b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx index 7ea45df..177f55b 100644 --- a/frontend/src/Pages/AdminPages/AdminManageProjects.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageProjects.tsx @@ -2,22 +2,9 @@ import { Link } from "react-router-dom"; import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; -import { ProjectListAdmin } from "../../Components/ProjectListAdmin"; -import { Project } from "../../Types/goTypes"; -import GetProjects from "../../Components/GetProjects"; -import { useState } from "react"; function AdminManageProjects(): JSX.Element { - const [projects, setProjects] = useState([]); - GetProjects({ setProjectsProp: setProjects }); - const content = ( - <> -

    Manage Projects

    -
    - -
    - - ); + const content = <>; const buttons = ( <> diff --git a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx index 893bdad..96167cb 100644 --- a/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx +++ b/frontend/src/Pages/AdminPages/AdminProjectAddMember.tsx @@ -1,10 +1,27 @@ -import AddUserToProject from "../../Components/AddUserToProject"; import BasicWindow from "../../Components/BasicWindow"; +import Button from "../../Components/Button"; function AdminProjectAddMember(): JSX.Element { - const content = ; + const content = <>; - const buttons = <>; + const buttons = ( + <> +