diff --git a/.gitignore b/.gitignore index 281e866..313b735 100644 --- a/.gitignore +++ b/.gitignore @@ -9,16 +9,11 @@ bin database.txt plantuml.jar db.sqlite3 -db.sqlite3-journal diagram.puml 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..432fbd1 100644 --- a/Justfile +++ b/Justfile @@ -15,7 +15,7 @@ remove-podman-containers: # Saves the release container to a tarball, pigz is just gzip but multithreaded save-release: build-container-release - podman save --format=oci-archive ttime-server | pigz -9 > ttime-server_`date -I`_`git rev-parse --short HEAD`.tar.gz + podman save --format=oci-archive ttime-server | pigz -9 > ttime-server.tar.gz # Loads the release container from a tarball load-release file: @@ -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 039340c..65a2f3c 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 @@ -39,16 +37,6 @@ clean: 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 ./... @@ -104,16 +92,6 @@ default: build docs: swag init -outputTypes go -api: ./docs/swagger.json - 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 @@ -152,12 +130,4 @@ install-just: .PHONY: types types: - tygo generate - -.PHONY: install-golds -install-golds: - go install go101.org/golds@latest - -.PHONY: golds -golds: - golds -port 6060 -nouses -plainsrc -wdpkgs-listing=promoted ./... + tygo generate \ No newline at end of file diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 0009c17..322c812 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -137,13 +137,13 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully promoted user", + "description": "Successfully prometed user", "schema": { "type": "json" } }, "400": { - "description": "Bad request", + "description": "bad request", "schema": { "type": "string" } diff --git a/backend/internal/config/config_test.go b/backend/internal/config/config_test.go index e8ddce8..cb02a31 100644 --- a/backend/internal/config/config_test.go +++ b/backend/internal/config/config_test.go @@ -5,12 +5,8 @@ import ( "testing" ) -// TestNewConfig tests the creation of a new configuration object func TestNewConfig(t *testing.T) { - // Arrange c := NewConfig() - - // Act & Assert if c.Port != 8080 { t.Errorf("Expected port to be 8080, got %d", c.Port) } @@ -28,15 +24,9 @@ func TestNewConfig(t *testing.T) { } } -// TestWriteConfig tests the function to write the configuration to a file func TestWriteConfig(t *testing.T) { - // Arrange c := NewConfig() - - //Act err := c.WriteConfigToFile("test.toml") - - // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } @@ -45,23 +35,14 @@ func TestWriteConfig(t *testing.T) { _ = os.Remove("test.toml") } -// TestReadConfig tests the function to read the configuration from a file func TestReadConfig(t *testing.T) { - // Arrange c := NewConfig() - - // Act err := c.WriteConfigToFile("test.toml") - - // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } - // Act c2, err := ReadConfigFromFile("test.toml") - - // Assert if err != nil { t.Errorf("Expected no error, got %s", err) } diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index f4c0f6e..bc6e1e8 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -3,7 +3,6 @@ package database import ( "embed" "errors" - "fmt" "path/filepath" "ttime/internal/types" @@ -20,14 +19,12 @@ type Database interface { PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error - DeleteProject(name string, username string) error Migrate() error MigrateSampleData() error GetProjectId(projectname string) (int, error) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error - ChangeUserName(username string, newname string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) @@ -35,14 +32,8 @@ type Database interface { GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) - GetWeeklyReportsUser(username string, projectname string) ([]types.WeeklyReportList, error) - GetUnsignedWeeklyReports(projectName string) ([]types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) - IsProjectManager(username string, projectname string) (bool, error) - GetProjectTimes(projectName string) (map[string]int, error) - UpdateWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error - RemoveProject(projectname string) error } // This struct is a wrapper type that holds the database connection @@ -64,27 +55,19 @@ var sampleData embed.FS // TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" -const projectInsert = "INSERT INTO projects (name, description, owner_user_id) VALUES (?, ?, (SELECT id FROM users WHERE username = ?))" +const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), ProjectLookup AS (SELECT id FROM projects WHERE name = ?) INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` -const addUserToProject = `INSERT OR IGNORE INTO user_roles (user_id, project_id, p_role) - VALUES ((SELECT id FROM users WHERE username = ?), - (SELECT id FROM projects WHERE name = ?), ?)` -const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)" +const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP +const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" + const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p JOIN user_roles ur ON p.id = ur.project_id JOIN users u ON ur.user_id = u.id WHERE u.username = ?` -const deleteProject = `DELETE FROM projects - WHERE id = ? AND owner_username = ?` - -const isProjectManagerQuery = `SELECT COUNT(*) > 0 FROM user_roles - JOIN users ON user_roles.user_id = users.id - JOIN projects ON user_roles.project_id = projects.id - WHERE users.username = ? AND projects.name = ? AND user_roles.p_role = 'project_manager'` // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -142,23 +125,42 @@ func (d *Db) AddWeeklyReport(projectName string, userName string, week int, deve } // AddUserToProject adds a user to a project with a specified role. -func (d *Db) AddUserToProject(username string, projectname string, role string) error { - _, err := d.Exec(addUserToProject, username, projectname, role) - return err +func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP + var userid int + userid, err := d.GetUserId(username) + if err != nil { + panic(err) + } + + var projectid int + projectid, err2 := d.GetProjectId(projectname) + if err2 != nil { + panic(err2) + } + + _, err3 := d.Exec(addUserToProject, userid, projectid, role) + return err3 } // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { - // Execute the SQL query to change the user's role - _, err := d.Exec(changeUserRole, role, username, projectname) - return err -} + // Get the user ID + var userid int + userid, err := d.GetUserId(username) + if err != nil { + panic(err) + } -// ChangeUserName changes the username of a user. -func (d *Db) ChangeUserName(username string, newname string) error { - // Execute the SQL query to update the username - _, err := d.Exec("UPDATE users SET username = ? WHERE username = ?", newname, username) - return err + // Get the project ID + var projectid int + projectid, err2 := d.GetProjectId(projectname) + if err2 != nil { + panic(err2) + } + + // Execute the SQL query to change the user's role + _, err3 := d.Exec(changeUserRole, role, userid, projectid) + return err3 } // GetUserRole retrieves the role of a user within a project. @@ -200,7 +202,6 @@ func (d *Db) GetProjectId(projectname string) (int, error) { // Creates a new project in the database, associated with a user func (d *Db) AddProject(name string, description string, username string) error { tx := d.MustBegin() - // Insert the project into the database _, err := tx.Exec(projectInsert, name, description, username) if err != nil { if err := tx.Rollback(); err != nil { @@ -208,9 +209,7 @@ func (d *Db) AddProject(name string, description string, username string) error } return err } - - // Add creator to project as project manager - _, err = tx.Exec(addUserToProject, username, name, "project_manager") + _, err = tx.Exec(changeUserRole, "project_manager", username, name) if err != nil { if err := tx.Rollback(); err != nil { return err @@ -224,21 +223,6 @@ func (d *Db) AddProject(name string, description string, username string) error return err } -func (d *Db) DeleteProject(projectID string, username string) error { - tx := d.MustBegin() - - _, err := tx.Exec(deleteProject, projectID, username) - - if err != nil { - if rollbackErr := tx.Rollback(); rollbackErr != nil { - return fmt.Errorf("error rolling back transaction: %v, delete error: %v", rollbackErr, err) - } - panic(err) - } - - return err -} - func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) { // Define the SQL query to fetch users and their roles for a given project query := ` @@ -356,51 +340,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 @@ -463,63 +402,6 @@ func (d *Db) Migrate() error { return nil } -// GetWeeklyReportsUser retrieves weekly reports for a specific user and project. -func (d *Db) GetWeeklyReportsUser(username string, projectName string) ([]types.WeeklyReportList, error) { - query := ` - SELECT - wr.week, - wr.development_time, - wr.meeting_time, - wr.admin_time, - wr.own_work_time, - wr.study_time, - wr.testing_time, - wr.signed_by - FROM - weekly_reports wr - INNER JOIN - users u ON wr.user_id = u.id - INNER JOIN - projects p ON wr.project_id = p.id - WHERE - u.username = ? AND p.name = ? - ` - - var reports []types.WeeklyReportList - if err := d.Select(&reports, query, username, projectName); err != nil { - return nil, err - } - - return reports, nil -} - -// IsProjectManager checks if a given username is a project manager for the specified project -func (d *Db) IsProjectManager(username string, projectname string) (bool, error) { - var manager bool - err := d.Get(&manager, isProjectManagerQuery, username, projectname) - 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 @@ -558,46 +440,3 @@ 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) { - query := ` - SELECT development_time, meeting_time, admin_time, own_work_time, study_time, testing_time - FROM weekly_reports - JOIN projects ON weekly_reports.project_id = projects.id - WHERE projects.name = ? - ` - - rows, err := d.DB.Query(query, projectName) - if err != nil { - return nil, err - } - defer rows.Close() - - totalTime := make(map[string]int) - - for rows.Next() { - var developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime int - if err := rows.Scan(&developmentTime, &meetingTime, &adminTime, &ownWorkTime, &studyTime, &testingTime); err != nil { - return nil, err - } - - totalTime["development"] += developmentTime - totalTime["meeting"] += meetingTime - totalTime["admin"] += adminTime - totalTime["own_work"] += ownWorkTime - totalTime["study"] += studyTime - totalTime["testing"] += testingTime - } - - if err := rows.Err(); err != nil { - return nil, err - } - - 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..a7f3878 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,12 +1,12 @@ package database import ( + "fmt" "testing" ) // Tests are not guaranteed to be sequential -// setupState initializes a database instance with necessary setup for testing func setupState() (Database, error) { db := DbConnect(":memory:") err := db.Migrate() @@ -16,62 +16,11 @@ func setupState() (Database, error) { return db, nil } -// This is a more advanced setup that includes more data in the database. -// This is useful for more complex testing scenarios. -func setupAdvancedState() (Database, error) { - db, err := setupState() - if err != nil { - return nil, err - } - - // Add a user - if err = db.AddUser("demouser", "password"); err != nil { - return nil, err - } - - // Add a project - if err = db.AddProject("projecttest", "description", "demouser"); err != nil { - return nil, err - } - - // Add a weekly report - if err = db.AddWeeklyReport("projecttest", "demouser", 1, 1, 1, 1, 1, 1, 1); err != nil { - return nil, err - } - - return db, nil -} - -// TestDbConnect tests the connection to the database func TestDbConnect(t *testing.T) { db := DbConnect(":memory:") _ = db } -func TestSetupAdvancedState(t *testing.T) { - db, err := setupAdvancedState() - if err != nil { - t.Error("setupAdvancedState failed:", err) - } - - // Check if the user was added - if _, err = db.GetUserId("demouser"); err != nil { - t.Error("GetUserId failed:", err) - } - - // Check if the project was added - projects, err := db.GetAllProjects() - if err != nil { - t.Error("GetAllProjects failed:", err) - } - if len(projects) != 1 { - t.Error("GetAllProjects failed: expected 1, got", len(projects)) - } - - // To be continued... -} - -// TestDbAddUser tests the AddUser function of the database func TestDbAddUser(t *testing.T) { db, err := setupState() if err != nil { @@ -83,7 +32,6 @@ func TestDbAddUser(t *testing.T) { } } -// TestDbGetUserId tests the GetUserID function of the database func TestDbGetUserId(t *testing.T) { db, err := setupState() if err != nil { @@ -104,20 +52,18 @@ func TestDbGetUserId(t *testing.T) { } } -// TestDbAddProject tests the AddProject function of the database func TestDbAddProject(t *testing.T) { - db, err := setupAdvancedState() + db, err := setupState() if err != nil { t.Error("setupState failed:", err) } - err = db.AddProject("test", "description", "demouser") + err = db.AddProject("test", "description", "test") if err != nil { t.Error("AddProject failed:", err) } } -// TestDbRemoveUser tests the RemoveUser function of the database func TestDbRemoveUser(t *testing.T) { db, err := setupState() if err != nil { @@ -130,7 +76,6 @@ func TestDbRemoveUser(t *testing.T) { } } -// TestPromoteToAdmin tests the PromoteToAdmin function of the database func TestPromoteToAdmin(t *testing.T) { db, err := setupState() if err != nil { @@ -148,7 +93,6 @@ func TestPromoteToAdmin(t *testing.T) { } } -// TestAddWeeklyReport tests the AddWeeklyReport function of the database func TestAddWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { @@ -171,7 +115,6 @@ func TestAddWeeklyReport(t *testing.T) { } } -// TestAddUserToProject tests the AddUseToProject function of the database func TestAddUserToProject(t *testing.T) { db, err := setupState() if err != nil { @@ -199,7 +142,6 @@ func TestAddUserToProject(t *testing.T) { } } -// TestChangeUserRole tests the ChangeUserRole function of the database func TestChangeUserRole(t *testing.T) { db, err := setupState() if err != nil { @@ -216,15 +158,20 @@ func TestChangeUserRole(t *testing.T) { t.Error("AddProject failed:", err) } + err = db.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + role, err := db.GetUserRole("testuser", "testproject") if err != nil { t.Error("GetUserRole failed:", err) } - if role != "project_manager" { - t.Error("GetUserRole failed: expected project_manager, got", role) + if role != "user" { + t.Error("GetUserRole failed: expected user, got", role) } - err = db.ChangeUserRole("testuser", "testproject", "member") + err = db.ChangeUserRole("testuser", "testproject", "admin") if err != nil { t.Error("ChangeUserRole failed:", err) } @@ -233,13 +180,12 @@ func TestChangeUserRole(t *testing.T) { if err != nil { t.Error("GetUserRole failed:", err) } - if role != "member" { - t.Error("GetUserRole failed: expected member, got", role) + if role != "admin" { + t.Error("GetUserRole failed: expected admin, got", role) } } -// TestGetAllUsersProject tests the GetAllUsersProject function of the database func TestGetAllUsersProject(t *testing.T) { db, err := setupState() if err != nil { @@ -306,7 +252,6 @@ func TestGetAllUsersProject(t *testing.T) { } } -// TestGetAllUsersApplication tests the GetAllUsersApplicsation function of the database func TestGetAllUsersApplication(t *testing.T) { db, err := setupState() if err != nil { @@ -353,7 +298,6 @@ func TestGetAllUsersApplication(t *testing.T) { } } -// TestGetProjectsForUser tests the GetProjectsForUser function of the database func TestGetProjectsForUser(t *testing.T) { db, err := setupState() if err != nil { @@ -394,7 +338,6 @@ func TestGetProjectsForUser(t *testing.T) { } } -// TestAddProject tests AddProject function of the database func TestAddProject(t *testing.T) { db, err := setupState() if err != nil { @@ -430,7 +373,6 @@ func TestAddProject(t *testing.T) { } } -// TestGetWeeklyReport tests GetWeeklyReport function of the database func TestGetWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { @@ -470,48 +412,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() if err != nil { @@ -564,6 +464,7 @@ func TestSignWeeklyReport(t *testing.T) { if err != nil { t.Error("GetUserId failed:", err) } + fmt.Println("Project Manager's ID:", projectManagerID) // Sign the report with the project manager err = db.SignWeeklyReport(report.ReportId, projectManagerID) @@ -583,7 +484,6 @@ func TestSignWeeklyReport(t *testing.T) { } } -// TestSignWeeklyReportByAnotherProjectManager tests the scenario where a project manager attempts to sign a weekly report for a user who is not assigned to their project func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { db, err := setupState() if err != nil { @@ -602,7 +502,7 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("AddUser failed:", err) } - // Add project, projectManager is the owner + // Add project err = db.AddProject("testproject", "description", "projectManager") if err != nil { t.Error("AddProject failed:", err) @@ -626,29 +526,17 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { t.Error("GetWeeklyReport failed:", err) } - managerID, err := db.GetUserId("projectManager") + anotherManagerID, err := db.GetUserId("projectManager") if err != nil { t.Error("GetUserId failed:", err) } - err = db.SignWeeklyReport(report.ReportId, managerID) - if err != nil { - t.Error("SignWeeklyReport failed:", err) - } - - // Retrieve the report again to check if it's signed - signedReport, err := db.GetWeeklyReport("testuser", "testproject", 1) - if err != nil { - t.Error("GetWeeklyReport failed:", err) - } - - // Ensure the report is signed by the project manager - if *signedReport.SignedBy != managerID { - t.Errorf("Expected SignedBy to be %d, got %d", managerID, *signedReport.SignedBy) + err = db.SignWeeklyReport(report.ReportId, anotherManagerID) + if err == nil { + t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") } } -// TestGetProject tests GetProject function of the database func TestGetProject(t *testing.T) { db, err := setupState() if err != nil { @@ -678,290 +566,3 @@ func TestGetProject(t *testing.T) { t.Errorf("Expected Name to be testproject, got %s", project.Name) } } - -func TestGetWeeklyReportsUser(t *testing.T) { - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - } - - err = db.AddUser("testuser", "password") - if err != nil { - t.Error("AddUser failed:", err) - } - - err = db.AddProject("testproject", "description", "testuser") - if err != nil { - t.Error("AddProject failed:", err) - } - - err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) - if err != nil { - t.Error("AddWeeklyReport failed:", err) - } - - err = db.AddWeeklyReport("testproject", "testuser", 2, 1, 1, 1, 1, 1, 1) - if err != nil { - t.Error("AddWeeklyReport failed:", err) - } - - reports, err := db.GetWeeklyReportsUser("testuser", "testproject") - if err != nil { - t.Error("GetWeeklyReportsUser failed:", err) - } - - // Check if the retrieved reports match the expected values - if len(reports) != 2 { - t.Errorf("Expected 1 report, got %d", len(reports)) - } -} - -func TestIsProjectManager(t *testing.T) { - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - } - - // Add a project manager - err = db.AddUser("projectManager", "password") - if err != nil { - t.Error("AddUser failed:", err) - } - - // Add a regular user - err = db.AddUser("testuser", "password") - if err != nil { - t.Error("AddUser failed:", err) - } - - // Add project - err = db.AddProject("testproject", "description", "projectManager") - if err != nil { - t.Error("AddProject failed:", err) - } - - // Add both regular users as members to the project - err = db.AddUserToProject("testuser", "testproject", "member") - if err != nil { - t.Error("AddUserToProject failed:", err) - } - - err = db.AddUserToProject("projectManager", "testproject", "project_manager") - if err != nil { - t.Error("AddUserToProject failed:", err) - } - - // Check if the regular user is not a project manager - isManager, err := db.IsProjectManager("testuser", "testproject") - if err != nil { - t.Error("IsProjectManager failed:", err) - } - if isManager { - t.Error("Expected testuser not to be a project manager, but it is.") - } - - // Check if the project manager is indeed a project manager - isManager, err = db.IsProjectManager("projectManager", "testproject") - if err != nil { - t.Error("IsProjectManager failed:", err) - } - if !isManager { - t.Error("Expected projectManager to be a project manager, but it's not.") - } -} - -func TestGetProjectTimes(t *testing.T) { - // Initialize - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - return - } - - // Create a user - user := "TeaUser" - password := "Vanilla" - err = db.AddUser(user, password) - if err != nil { - t.Error("AddUser failed:", err) - return - } - - // Create a project - projectName := "ProjectVanilla" - projectDescription := "When tea tastes its best" - err = db.AddProject(projectName, projectDescription, user) // Fix the variable name here - if err != nil { - t.Error("AddProject failed:", err) - return - } - - // Tests the func in db.go - totalTime, err := db.GetProjectTimes(projectName) - if err != nil { - t.Error("GetTotalTimePerActivity failed:", err) - return - } - - // 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]) - } - } -} -func TestEnsureManagerOfCreatedProject(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) - } - - // Set user to a project manager - // err = db.AddUserToProject("testuser", "testproject", "project_manager") - // if err != nil { - // t.Error("AddUserToProject failed:", err) - // } - - managerState, err := db.IsProjectManager("testuser", "testproject") - if err != nil { - t.Error("IsProjectManager failed:", err) - } - - if !managerState { - 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/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql index b0cbe82..8f76b80 100644 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS weekly_reports ( study_time INTEGER, testing_time INTEGER, signed_by INTEGER, - UNIQUE(user_id, project_id, week), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (project_id) REFERENCES projects(id), FOREIGN KEY (signed_by) REFERENCES users(id) diff --git a/backend/internal/database/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql index ab74f1a..4dac91b 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); @@ -35,18 +33,3 @@ VALUES (3,3,"member"); INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) VALUES (2,1,"project_manager"); - -INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (2, 1, 12, 20, 10, 5, 30, 15, 10, NULL); - -INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (3, 1, 12, 20, 10, 5, 30, 15, 10, NULL); - -INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (3, 1, 14, 20, 10, 5, 30, 15, 10, NULL); - -INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (3, 2, 12, 20, 10, 5, 30, 15, 10, NULL); - -INSERT OR IGNORE INTO weekly_reports (user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by) -VALUES (3, 3, 12, 20, 10, 5, 30, 15, 10, NULL); diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go new file mode 100644 index 0000000..932451d --- /dev/null +++ b/backend/internal/handlers/global_state.go @@ -0,0 +1,50 @@ +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 + // GetProject(c *fiber.Ctx) error // To get a specific project + // UpdateProject(c *fiber.Ctx) error // To update a project + // DeleteProject(c *fiber.Ctx) error // To delete a project + // CreateTask(c *fiber.Ctx) error // To create a new task + // GetTasks(c *fiber.Ctx) error // To get all tasks + // GetTask(c *fiber.Ctx) error // To get a specific task + // UpdateTask(c *fiber.Ctx) error // To update a task + // DeleteTask(c *fiber.Ctx) error // To delete a task + // CreateCollection(c *fiber.Ctx) error // To create a new collection + // GetCollections(c *fiber.Ctx) error // To get all collections + // GetCollection(c *fiber.Ctx) error // To get a specific collection + // UpdateCollection(c *fiber.Ctx) error // To update a collection + // DeleteCollection(c *fiber.Ctx) error // To delete a collection + // SignCollection(c *fiber.Ctx) error // To sign a collection + ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database + ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project + ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project +} + +// "Constructor" +func NewGlobalState(db database.Database) GlobalState { + return &GState{Db: db} +} + +// 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..f3a7ea0 --- /dev/null +++ b/backend/internal/handlers/handlers_project_related.go @@ -0,0 +1,156 @@ +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") +} + +// GetUserProjects returns all projects that the user is a member of +func (gs *GState) GetUserProjects(c *fiber.Ctx) error { + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Then dip into the database to get the projects + projects, err := gs.Db.GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} + +// ProjectRoleChange is a handler that changes a user's role within a project +func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error { + // Extract the necessary parameters from the request + username := c.Params("username") + projectName := c.Params("projectName") + role := c.Params("role") + + // Change the user's role within the project in the database + if err := gs.Db.ChangeUserRole(username, projectName, role); err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a success message + return c.SendStatus(fiber.StatusOK) +} + +// GetProject retrieves a specific project by its ID +func (gs *GState) GetProject(c *fiber.Ctx) error { + // Extract the project ID from the request parameters or body + projectID := c.Params("projectID") + if projectID == "" { + log.Info("No project ID provided") + return c.Status(400).SendString("No project ID provided") + } + log.Info("Getting project with ID: ", projectID) + + // Parse the project ID into an integer + projectIDInt, err := strconv.Atoi(projectID) + if err != nil { + log.Info("Invalid project ID") + return c.Status(400).SendString("Invalid project ID") + } + + // Get the project from the database by its ID + project, err := gs.Db.GetProject(projectIDInt) + if err != nil { + log.Info("Error getting project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return the project as JSON + log.Info("Returning project: ", project.Name) + return c.JSON(project) +} + +func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { + // Extract the project name from the request parameters or body + projectName := c.Params("projectName") + if projectName == "" { + log.Info("No project name provided") + return c.Status(400).SendString("No project name provided") + } + + // Get all users associated with the project from the database + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + log.Info("Error getting users for project:", err) + return c.Status(500).SendString(err.Error()) + } + + log.Info("Returning users for project: ", projectName) + + // Return the list of users as JSON + return c.JSON(users) +} + +// AddUserToProjectHandler is a handler that adds a user to a project with a specified role +func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { + // Extract necessary parameters from the request + var requestData struct { + Username string `json:"username"` + ProjectName string `json:"projectName"` + Role string `json:"role"` + } + if err := c.BodyParser(&requestData); err != nil { + log.Info("Error parsing request body:", err) + return c.Status(400).SendString("Bad request") + } + + // Check if the user adding another user to the project is a site admin + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + adminUsername := claims["name"].(string) + log.Info("Admin username from claims:", adminUsername) + + isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) + if err != nil { + log.Info("Error checking admin status:", err) + return c.Status(500).SendString(err.Error()) + } + + if !isAdmin { + log.Info("User is not a site admin:", adminUsername) + return c.Status(403).SendString("User is not a site admin") + } + + // Add the user to the project with the specified role + err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) + if err != nil { + log.Info("Error adding user to project:", err) + return c.Status(500).SendString(err.Error()) + } + + // Return success message + log.Info("User added to project successfully:", requestData.Username) + return c.SendStatus(fiber.StatusOK) +} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go new file mode 100644 index 0000000..85eb6e2 --- /dev/null +++ b/backend/internal/handlers/handlers_report_related.go @@ -0,0 +1,116 @@ +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") +} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go new file mode 100644 index 0000000..96fddb7 --- /dev/null +++ b/backend/internal/handlers/handlers_user_related.go @@ -0,0 +1,216 @@ +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) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": u.Username, + "admin": false, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + 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) +} + +// @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) +} 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 1cae76c..0000000 --- a/backend/internal/handlers/users/ListAllUsers.go +++ /dev/null @@ -1,31 +0,0 @@ -package users - -import ( - db "ttime/internal/database" - - "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" -) - -// ListAllUsers is a handler that returns a list of all users in the application database -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {json} json "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] -func ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := db.GetDb(c).GetAllUsersApplication() - if err != nil { - log.Info("Error getting users from db:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("Returning all users") - // Return the list of users as JSON - return c.JSON(users) -} diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go deleted file mode 100644 index c4d6c60..0000000 --- a/backend/internal/handlers/users/Login.go +++ /dev/null @@ -1,65 +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" -) - -// Login is a simple login handler that returns a JWT token -// @Summary login -// @Description logs the user in and returns a jwt token -// @Tags User -// @Accept json -// @Param NewUser body types.NewUser true "login info" -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] -func Login(c *fiber.Ctx) error { - // The body type is identical to a NewUser - - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Username logging in:", u.Username) - if !db.GetDb(c).CheckUser(u.Username, u.Password) { - log.Info("User not found") - return c.SendStatus(fiber.StatusUnauthorized) - } - - isAdmin, err := db.GetDb(c).IsSiteAdmin(u.Username) - if err != nil { - log.Info("Error checking admin status:", err) - return c.Status(500).SendString(err.Error()) - } - // Create the Claims - claims := jwt.MapClaims{ - "name": u.Username, - "admin": isAdmin, - "exp": time.Now().Add(time.Hour * 72).Unix(), - } - - // Create token - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - log.Info("Token created for user:", u.Username) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - println("Successfully signed token for user:", u.Username) - return c.JSON(types.Token{Token: t}) -} diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go deleted file mode 100644 index 78eadfd..0000000 --- a/backend/internal/handlers/users/LoginRenew.go +++ /dev/null @@ -1,44 +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" -) - -// LoginRenew is a simple handler that renews the token -// @Summary LoginRenews -// @Description renews the users token -// @Security bererToken -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginerenew [post] -func LoginRenew(c *fiber.Ctx) error { - user := c.Locals("user").(*jwt.Token) - - log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - - claims := user.Claims.(jwt.MapClaims) - claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - renewed := jwt.MapClaims{ - "name": claims["name"], - "admin": claims["admin"], - "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) - t, err := token.SignedString([]byte("secret")) - if err != nil { - log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) - return c.JSON(types.Token{Token: t}) -} diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go deleted file mode 100644 index 4a21758..0000000 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ /dev/null @@ -1,42 +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 admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] -func PromoteToAdmin(c *fiber.Ctx) error { - // Extract the username from the request body - var newUser types.NewUser - if err := c.BodyParser(&newUser); err != nil { - return c.Status(400).SendString("Bad request") - } - username := newUser.Username - - log.Info("Promoting user to admin:", username) // Debug print - - // Promote the user to a site admin in the database - if err := db.GetDb(c).PromoteToAdmin(username); err != nil { - log.Info("Error promoting user to admin:", err) // Debug print - return c.Status(500).SendString(err.Error()) - } - - log.Info("User promoted to admin successfully:", username) // Debug print - - // Return a success message - return c.SendStatus(fiber.StatusOK) -} diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go deleted file mode 100644 index 9977246..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" -) - -// Register is a simple handler that registers a new user -// -// @Summary Register -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "User to register" -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /register [post] -func Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - log.Info("Adding user:", u.Username) - if err := db.GetDb(c).AddUser(u.Username, u.Password); err != nil { - log.Warn("Error adding user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User added:", u.Username) - return c.Status(200).SendString("User added") -} diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go deleted file mode 100644 index 5957c2d..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" -) - -// This path should obviously be protected in the future -// UserDelete deletes a user from the database -// -// @Summary UserDelete -// @Description UserDelete deletes a user from the database -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {string} string "User deleted" -// @Failure 403 {string} string "You can only delete yourself" -// @Failure 500 {string} string "Internal server error" -// @Failure 401 {string} string "Unauthorized" -// @Router /userdelete/{username} [delete] -func UserDelete(c *fiber.Ctx) error { - // Read from path parameters - username := c.Params("username") - - // Read username from Locals - auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) - - if username == auth_username { - log.Info("User tried to delete itself") - return c.Status(403).SendString("You can't delete yourself") - } - - if err := db.GetDb(c).RemoveUser(username); err != nil { - log.Warn("Error deleting user:", err) - return c.Status(500).SendString(err.Error()) - } - - log.Info("User deleted:", username) - return c.Status(200).SendString("User deleted") -} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go index 234781b..299395a 100644 --- a/backend/internal/types/WeeklyReport.go +++ b/backend/internal/types/WeeklyReport.go @@ -20,27 +20,6 @@ type NewWeeklyReport struct { TestingTime int `json:"testingTime"` } -type WeeklyReportList struct { - // The name of the project, as it appears in the database - ProjectName string `json:"projectName" db:"project_name"` - // The week number - Week int `json:"week" db:"week"` - // Total time spent on development - DevelopmentTime int `json:"developmentTime" db:"development_time"` - // Total time spent in meetings - MeetingTime int `json:"meetingTime" db:"meeting_time"` - // Total time spent on administrative tasks - AdminTime int `json:"adminTime" db:"admin_time"` - // Total time spent on personal projects - OwnWorkTime int `json:"ownWorkTime" db:"own_work_time"` - // Total time spent on studying - StudyTime int `json:"studyTime" db:"study_time"` - // Total time spent on testing - TestingTime int `json:"testingTime" db:"testing_time"` - // The project manager who signed it - SignedBy *int `json:"signedBy" db:"signed_by"` -} - type WeeklyReport struct { // The ID of the report ReportId int `json:"reportId" db:"report_id"` @@ -65,24 +44,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/project.go b/backend/internal/types/project.go index 2e26eb9..7e1747f 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -13,17 +13,3 @@ type NewProject struct { Name string `json:"name"` Description string `json:"description"` } - -// Used to change the role of a user in a project. -// If name is identical to the name contained in the token, the role can be changed. -// If the name is different, only a project manager can change the role. -type RoleChange struct { - UserName string `json:"username"` - Role string `json:"role" tstype:"'project_manager' | 'user'"` - Projectname string `json:"projectname"` -} - -type NameChange struct { - ID int `json:"id" db:"id"` - Name string `json:"name" db:"name"` -} diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 88b4f06..d3f2170 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -32,8 +32,3 @@ type PublicUser struct { type Token struct { Token string `json:"token"` } - -type StrNameChange struct { - PrevName string `json:"prevName" db:"prevName"` - NewName string `json:"newName" db:"newName"` -} diff --git a/backend/main.go b/backend/main.go index 4c2056e..e578c52 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" @@ -35,12 +33,6 @@ import ( // @externalDocs.description OpenAPI // @externalDocs.url https://swagger.io/resources/open-api/ -/** -Main function for starting the server and initializing configurations. -Reads configuration from file, pretty prints it, connects to the database, -migrates it, and sets up routes for the server. -*/ - func main() { conf, err := config.ReadConfigFromFile("config.toml") if err != nil { @@ -56,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) @@ -85,51 +73,27 @@ 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.Post("/api/project", gs.CreateProject) + server.Get("/api/project/:projectId", gs.GetProject) + server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) + server.Post("/api/signReport", gs.SignReport) + server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) + server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) + server.Get("/api/users/all", gs.ListAllUsers) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) if err != nil { 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/src/API/API.ts b/frontend/src/API/API.ts index 0a85e70..8fd66d3 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -4,167 +4,50 @@ import { User, Project, NewProject, - UserProjectMember, - WeeklyReport, - StrNameChange, - NewProjMember, } from "../Types/goTypes"; -/** - * Response object returned by API methods. - */ +// This type of pattern should be hard to misuse export interface APIResponse { - /** Indicates whether the API call was successful */ success: boolean; - /** Optional message providing additional information or error description */ message?: string; - /** Optional data returned by the API method */ data?: T; } -/** - * Interface defining methods that an instance of the API must implement. - */ +// Note that all protected routes also require a token +// Defines all the methods that an instance of the API must implement interface API { - /** - * Register a new user - * @param {NewUser} user The user object to be registered - * @returns {Promise>} A promise containing the API response with the user data. - */ + /** Register a new user */ registerUser(user: NewUser): Promise>; - - /** - * Removes a user. - * @param {string} username The username of the user to be removed. - * @param {string} token The authentication token. - * @returns {Promise>} A promise containing the API response with the removed user data. - */ + /** Remove a user */ removeUser(username: string, token: string): Promise>; - - /** - * Check if user is project manager. - * @param {string} username The username of the user. - * @param {string} projectName The name of the project. - * @param {string} token The authentication token. - * @returns {Promise>} A promise containing the API response indicating if the user is a project manager. - */ - checkIfProjectManager( - projectName: string, - token: string, - ): Promise>; - - /** Logs in a user with the provided credentials. - * @param {NewUser} NewUser The user object containing username and password. - * @returns {Promise>} A promise resolving to an API response with a token. - */ + /** Login */ login(NewUser: NewUser): Promise>; - - /** - * Renew the token - * @param {string} token The current authentication token. - * @returns {Promise>} A promise resolving to an API response with a renewed token. - */ + /** Renew the token */ renewToken(token: string): Promise>; - - /** Promote user to admin */ - - /** Creates a new project. - * @param {NewProject} project The project object containing name and description. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response with the created project. - */ + /** Create a project */ createProject( project: NewProject, token: string, ): Promise>; - - /** Submits a weekly report - * @param {NewWeeklyReport} weeklyReport The weekly report object. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response with the submitted report. - */ + /** Submit a weekly report */ submitWeeklyReport( - weeklyReport: NewWeeklyReport, + project: NewWeeklyReport, token: string, - ): Promise>; - - /** Gets a weekly report for a specific user, project and week - * @param {string} projectName The name of the project. - * @param {string} week The week number. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response with the retrieved report. - */ + ): Promise>; + /**Gets a weekly report*/ getWeeklyReport( + username: string, projectName: string, week: string, token: string, - ): Promise>; - - /** - * Returns all the weekly reports for a user in a particular project - * The username is derived from the token - * @param {string} projectName The name of the project - * @param {string} token The token of the user - * @returns {APIResponse} A list of weekly reports - */ - getWeeklyReportsForUser( - projectName: string, - token: string, - ): Promise>; - - /** Gets all the projects of a user - * @param {string} token - The authentication token. - * @returns {Promise>} A promise containing the API response with the user's projects. - */ + ): Promise>; + /** Gets all the projects of a user*/ getUserProjects(token: string): Promise>; - - /** Gets a project by its id. - * @param {number} id The id of the project to retrieve. - * @returns {Promise>} A promise resolving to an API response containing the project data. - */ + /** Gets a project from id*/ getProject(id: number): Promise>; - - /** Gets a list of all users. - * @param {string} token The authentication token of the requesting user. - * @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 */ +// Export an instance of the API export const api: API = { async registerUser(user: NewUser): Promise> { try { @@ -198,51 +81,23 @@ export const api: API = { token: string, ): Promise> { try { - const response = await fetch(`/api/userdelete/${username}`, { - method: "DELETE", + const response = await fetch("/api/userdelete", { + 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" }; - } else { - return { success: true }; - } - } catch (e) { - return { success: false, message: "Failed to remove user" }; - } - }, - - async checkIfProjectManager( - projectName: string, - token: string, - ): Promise> { - try { - const response = await fetch( - `/api/checkIfProjectManager/${projectName}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }, - ); if (!response.ok) { - return { - success: false, - message: "Failed to check if project manager", - }; + return { success: false, message: "Failed to remove user" }; } else { - const data = (await response.json()) as boolean; + const data = (await response.json()) as User; return { success: true, data }; } } catch (e) { - return { success: false, message: "Failed to check if project manager" }; + return { success: false, message: "Failed to remove user" }; } }, @@ -271,30 +126,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", { @@ -346,7 +177,7 @@ export const api: API = { async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/submitWeeklyReport", { method: "POST", @@ -364,8 +195,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, @@ -375,62 +206,29 @@ export const api: API = { }, async getWeeklyReport( + username: string, projectName: string, week: string, token: string, - ): Promise> { + ): Promise> { try { - const response = await fetch( - `/api/getWeeklyReport?projectName=${projectName}&week=${week}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }, - ); - - if (!response.ok) { - return { success: false, message: "Failed to get weekly report" }; - } else { - const data = (await response.json()) as WeeklyReport; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to get weekly report" }; - } - }, - - async getWeeklyReportsForUser( - projectName: string, - token: string, - ): Promise> { - try { - const response = await fetch(`/api/getWeeklyReportsUser/${projectName}`, { + const response = await fetch("/api/getWeeklyReport", { method: "GET", headers: { "Content-Type": "application/json", Authorization: "Bearer " + token, }, + body: JSON.stringify({ username, projectName, week }), }); if (!response.ok) { - return { - success: false, - message: - "Failed to get weekly reports for project: Response code " + - response.status, - }; + return { success: false, message: "Failed to get weekly report" }; } else { - const data = (await response.json()) as WeeklyReport[]; + const data = (await response.json()) as NewWeeklyReport; return { success: true, data }; } } catch (e) { - return { - success: false, - message: "Failed to get weekly reports for project, unknown error", - }; + return { success: false, message: "Failed to get weekly report" }; } }, @@ -455,6 +253,7 @@ export const api: API = { } }, + // Gets a projet by id, currently untested since we have no javascript-based tests async getProject(id: number): Promise> { try { const response = await fetch(`/api/project/${id}`, { @@ -479,138 +278,4 @@ export const api: API = { }; } }, - - async getAllUsers(token: string): Promise> { - try { - const response = await fetch("/api/users/all", { - 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 string[]; - return Promise.resolve({ success: true, data }); - } - } catch (e) { - return Promise.resolve({ - success: false, - message: "API is not ok", - }); - } - }, - //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/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 f5f4a08..45814e3 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -7,7 +7,7 @@ import Button from "./Button"; /** * Tries to add a project to the system - * @param {Object} props - Project name and description + * @param props - Project name and description * @returns {boolean} True if created, false if not */ function CreateProject(props: { name: string; description: string }): boolean { @@ -34,8 +34,8 @@ 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 + * Tries to add a project to the system + * @returns {JSX.Element} UI for project adding */ function AddProject(): JSX.Element { const [name, setName] = useState(""); 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..067712e 100644 --- a/frontend/src/Components/AllTimeReportsInProject.tsx +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -1,36 +1,74 @@ -//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 { WeeklyReport } from "../Types/goTypes"; +import React, { useEffect, useState } from "react"; +import { NewWeeklyReport } from "../Types/goTypes"; import { Link, useParams } from "react-router-dom"; -import { api } from "../API/API"; -/** - * Renders a component that displays all the time reports for a specific project. - * @returns {JSX.Element} representing the component. - */ function AllTimeReportsInProject(): JSX.Element { const { projectName } = useParams(); - const [weeklyReports, setWeeklyReports] = useState([]); + const [weeklyReports, setWeeklyReports] = useState([]); + + /* const getWeeklyReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getWeeklyReports(token); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } +}; */ + + const getWeeklyReports = async (): Promise => { + const report: NewWeeklyReport[] = [ + { + projectName: projectName ?? "", + week: 10, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 1, + }, + { + projectName: projectName ?? "", + week: 11, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 100, + studyTime: 1, + testingTime: 1, + }, + { + projectName: projectName ?? "", + week: 12, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 1000, + }, + { + projectName: projectName ?? "", + week: 20, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 10000, + }, + // Add more reports as needed + ]; + setWeeklyReports(report); + await Promise.resolve(); + }; // 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 ( <> @@ -58,7 +96,7 @@ function AllTimeReportsInProject(): JSX.Element {

{"Signed: "} - {newWeeklyReport.signedBy ? "YES" : "NO"} + YES

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/AuthorizedRoute.tsx b/frontend/src/Components/AuthorizedRoute.tsx deleted file mode 100644 index d9c8c59..0000000 --- a/frontend/src/Components/AuthorizedRoute.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Navigate } from "react-router-dom"; -import React from "react"; - -interface AuthorizedRouteProps { - children: React.ReactNode; - isAuthorized: boolean; -} - -export function AuthorizedRoute({ - children, - isAuthorized, -}: AuthorizedRouteProps): JSX.Element { - if (!isAuthorized) { - return ; - } - - return children as React.ReactElement; -} diff --git a/frontend/src/Components/BackButton.tsx b/frontend/src/Components/BackButton.tsx index 4f58140..7a1ac81 100644 --- a/frontend/src/Components/BackButton.tsx +++ b/frontend/src/Components/BackButton.tsx @@ -1,11 +1,5 @@ -//info: Back button component to navigate back to the previous page import { useNavigate } from "react-router-dom"; -/** - * Renders a back button component. - * - * @returns The JSX element representing the back button. - */ function BackButton(): JSX.Element { const navigate = useNavigate(); const goBack = (): void => { diff --git a/frontend/src/Components/BackgroundAnimation.tsx b/frontend/src/Components/BackgroundAnimation.tsx index 87fca9e..5f402c0 100644 --- a/frontend/src/Components/BackgroundAnimation.tsx +++ b/frontend/src/Components/BackgroundAnimation.tsx @@ -1,10 +1,5 @@ -//info: Background animation component to animate the background of loginpage import { useEffect } from "react"; -/** - * Renders a background animation component. - * This component pre-loads images and starts a background transition animation. - */ const BackgroundAnimation = (): JSX.Element => { useEffect(() => { const images = [ diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx index d53d367..d5fd3b6 100644 --- a/frontend/src/Components/BasicWindow.tsx +++ b/frontend/src/Components/BasicWindow.tsx @@ -1,16 +1,6 @@ -//info: Basic window component to display content and buttons of a page, inclduing header and footer -//content to insert is placed in the content prop, and buttons in the buttons prop import Header from "./Header"; import Footer from "./Footer"; -/** - * Renders a basic window component with a header, content, and footer. - * - * @param {Object} props - The component props. - * @param {React.ReactNode} props.content - The content to be rendered in the window. - * @param {React.ReactNode} props.buttons - The buttons to be rendered in the footer. - * @returns {JSX.Element} The rendered basic window component. - */ function BasicWindow({ content, buttons, diff --git a/frontend/src/Components/Button.tsx b/frontend/src/Components/Button.tsx index 13ae807..38a1853 100644 --- a/frontend/src/Components/Button.tsx +++ b/frontend/src/Components/Button.tsx @@ -1,12 +1,3 @@ -/** - * Button component to display a button with text and onClick function. - * - * @param {Object} props - The component props. - * @param {string} props.text - The text to display on the button. - * @param {Function} props.onClick - The function to run when the button is clicked. - * @param {"submit" | "button" | "reset"} props.type - The type of button. - * @returns {JSX.Element} The rendered Button component. - */ function Button({ text, onClick, diff --git a/frontend/src/Components/ChangeRoles.tsx b/frontend/src/Components/ChangeRoles.tsx deleted file mode 100644 index e11d623..0000000 --- a/frontend/src/Components/ChangeRoles.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useState } from "react"; -import { useParams } from "react-router-dom"; -import Button from "./Button"; - -export default function ChangeRoles(): JSX.Element { - const [selectedRole, setSelectedRole] = useState(""); - const { username } = useParams(); - - const handleRoleChange = ( - event: React.ChangeEvent, - ): void => { - setSelectedRole(event.target.value); - }; - - // const handleSubmit = async (event: React.FormEvent) => { - // event.preventDefault(); - - // const response = await api.changeRole(username, selectedRole, token); - // if (response.success) { - // console.log("Role changed successfully"); - // } else { - // console.error("Failed to change role:", response.message); - // } - // }; - - return ( - <> -

- Change roll for: {username} -

-
-
-
- -
-
- -
-
- -
-
-
); } diff --git a/frontend/src/Components/CountButton.tsx b/frontend/src/Components/CountButton.tsx new file mode 100644 index 0000000..a6f1b30 --- /dev/null +++ b/frontend/src/Components/CountButton.tsx @@ -0,0 +1,38 @@ +import { useState, useEffect } from "react"; + +// Interface for the response from the server +// This should eventually reside in a dedicated file +interface CountResponse { + pressCount: number; +} + +// Some constants for the button +const BUTTON_ENDPOINT = "/api/button"; + +// A simple button that counts how many times it's been pressed +export function CountButton(): JSX.Element { + const [count, setCount] = useState(NaN); + + // useEffect with a [] dependency array runs only once + useEffect(() => { + async function getCount(): Promise { + const response = await fetch(BUTTON_ENDPOINT); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void getCount(); + }, []); + + // This is what runs on every button click + function press(): void { + async function pressPost(): Promise { + const response = await fetch(BUTTON_ENDPOINT, { method: "POST" }); + const data = (await response.json()) as CountResponse; + setCount(data.pressCount); + } + void pressPost(); + } + + // Return some JSX with the button and associated handler + return ; +} 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 deleted file mode 100644 index 494e6b0..0000000 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { useState, useEffect } from "react"; -import { Project } from "../Types/goTypes"; -import { useNavigate } from "react-router-dom"; -import { api } from "../API/API"; - -/** - * 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 [projects, setProjects] = useState([]); - const navigate = useNavigate(); - - const getProjects = async (): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(token); - console.log(response); - if (response.success) { - setProjects(response.data ?? []); - } else { - console.error(response.message); - } - }; - - const handleProjectClick = async (projectName: string): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.checkIfProjectManager(projectName, token); - console.log(response.data); - if (response.success) { - if (response.data === true) { - navigate(`/PMProjectPage/${projectName}`); - } else { - navigate(`/project/${projectName}`); - } - } else { - // handle error - console.error(response.message); - } - }; - - // Call getProjects when the component mounts - useEffect(() => { - void getProjects(); - }, []); - - return ( - <> -

Your Projects

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

- {project.name} -

-
- ))} -
- - ); -} - -export default DisplayUserProject; diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index 384359e..3017204 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,13 +1,9 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; -/** - * Renders the component for editing a weekly report. - * @returns JSX.Element - */ export default function GetWeeklyReport(): JSX.Element { const [week, setWeek] = useState(0); const [developmentTime, setDevelopmentTime] = useState(0); @@ -18,47 +14,45 @@ 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: NewWeeklyReport = response.data ?? { + projectName: "", + 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 +73,6 @@ export default function GetWeeklyReport(): JSX.Element { return ( <> -

Edit Time Report

{ @@ -94,10 +87,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 +124,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 +143,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 +162,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 +181,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 +200,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 +219,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/Footer.tsx b/frontend/src/Components/Footer.tsx index 192926f..a3b7469 100644 --- a/frontend/src/Components/Footer.tsx +++ b/frontend/src/Components/Footer.tsx @@ -1,13 +1,5 @@ -//info: Footer component to display the footer of a page where the buttons are placed import React from "react"; -/** - * Footer component. - * - * @param {Object} props - The component props. - * @param {React.ReactNode} props.children - The children elements to render inside the footer (buttons). - * @returns {JSX.Element} The rendered footer component. - */ function Footer({ children }: { children: React.ReactNode }): JSX.Element { return (
diff --git a/frontend/src/Components/GetAllUsers.tsx b/frontend/src/Components/GetAllUsers.tsx deleted file mode 100644 index 73ad244..0000000 --- a/frontend/src/Components/GetAllUsers.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Dispatch, useEffect } from "react"; -import { api } from "../API/API"; - -/** - * Gets all usernames in the system and puts them in an array - * @param props - A setStateAction for the array you want to put users in - * @returns {void} Nothing - * @example - * const [users, setUsers] = useState([]); - * GetAllUsers({ setUsersProp: setUsers }); - */ -function GetAllUsers(props: { - setUsersProp: Dispatch>; -}): void { - const setUsers: Dispatch> = props.setUsersProp; - useEffect(() => { - const fetchUsers = async (): Promise => { - try { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getAllUsers(token); - if (response.success) { - setUsers(response.data ?? []); - } else { - console.error("Failed to fetch users:", response.message); - } - } catch (error) { - console.error("Error fetching users:", error); - } - }; - - void fetchUsers(); - }, [setUsers]); -} - -export default GetAllUsers; 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/Header.tsx b/frontend/src/Components/Header.tsx index eb4fa5a..5cdb421 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,12 +1,7 @@ -//info: Header component to display the header of the page including the logo and user information where thr user can logout import { useState } from "react"; import { Link } from "react-router-dom"; import backgroundImage from "../assets/1.jpg"; -/** - * Renders the header component. - * @returns JSX.Element representing the header component. - */ function Header(): JSX.Element { const [isOpen, setIsOpen] = useState(false); diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx index f44d7f3..ce7d52c 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -32,11 +32,16 @@ function LoginCheck(props: { prevAuth = 1; return prevAuth; }); - } else if (token !== "") { + } else if (token !== "" && props.username === "pm") { props.setAuthority((prevAuth) => { prevAuth = 2; return prevAuth; }); + } else if (token !== "" && props.username === "user") { + props.setAuthority((prevAuth) => { + prevAuth = 3; + return prevAuth; + }); } } else { console.error("Token was undefined"); diff --git a/frontend/src/Components/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index f684b0c..e53823d 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -1,114 +1,71 @@ -//info: New weekly report form component to create a new weekly report to -//sumbit development time, meeting time, admin time, own work time, study time and testing time import { useState } from "react"; import type { NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; -/** - * Renders a form for creating a new weekly report. - * @returns The JSX element representing the new weekly report form. - */ 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 +85,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 +104,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 +123,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 +142,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 +161,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 +180,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/PMProjectMenu.tsx b/frontend/src/Components/PMProjectMenu.tsx deleted file mode 100644 index ce7c5c5..0000000 --- a/frontend/src/Components/PMProjectMenu.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { Link, useParams } from "react-router-dom"; -import { JSX } from "react/jsx-runtime"; - -function PMProjectMenu(): JSX.Element { - const { projectName } = useParams(); - return ( - <> -

{projectName}

-
- -

- Your Time Reports -

- - -

- New Time Report -

- - -

- Statistics -

- - -

- Unsigned Time Reports -

- -
- - ); -} -export default PMProjectMenu; 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 deleted file mode 100644 index 60ffcd9..0000000 --- a/frontend/src/Components/ProjectMembers.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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([]); - - useEffect(() => { - const getProjectMembers = async (): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getAllUsersProject(projectName ?? "", token); - console.log(response); - if (response.success) { - setProjectMembers(response.data ?? []); - } else { - console.error(response.message); - } - }; - - void getProjectMembers(); - }, [projectName]); - - interface ProjectMember { - Username: string; - UserRole: string; - } - - return ( - <> -

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

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

    -
    -
    -

    {projectMember.Username}

    - Role: -

    {projectMember.UserRole}

    -
    -
    -
    - -

    - View Reports -

    - -
    -
    -
    -

    - ))} -
    - - ); -} - -export default ProjectMembers; diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 6192637..7b003cb 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -4,16 +4,15 @@ 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. - * @returns The JSX element representing the registration form. - */ export default function Register(): JSX.Element { const [username, setUsername] = useState(); const [password, setPassword] = useState(); const [errMessage, setErrMessage] = useState(); + const nav = useNavigate(); + const handleRegister = async (): Promise => { const newUser: NewUser = { username: username ?? "", @@ -21,9 +20,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/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/AdminManageUsers.tsx b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx index 353fddc..0939d77 100644 --- a/frontend/src/Pages/AdminPages/AdminManageUsers.tsx +++ b/frontend/src/Pages/AdminPages/AdminManageUsers.tsx @@ -2,13 +2,15 @@ import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; import BackButton from "../../Components/BackButton"; import { UserListAdmin } from "../../Components/UserListAdmin"; +import { PublicUser } from "../../Types/goTypes"; import { useNavigate } from "react-router-dom"; -import GetAllUsers from "../../Components/GetAllUsers"; -import { useState } from "react"; function AdminManageUsers(): JSX.Element { - const [users, setUsers] = useState([]); - GetAllUsers({ setUsersProp: setUsers }); + //TODO: Change so that it reads users from database + const users: PublicUser[] = []; + for (let i = 1; i <= 20; i++) { + users.push({ userId: "id" + i, username: "Example User " + i }); + } const navigate = useNavigate(); 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 = ( + <> +
    - ); -} diff --git a/frontend/src/Pages/UserPages/UserEditTimeReportPage.tsx b/frontend/src/Pages/UserPages/UserEditTimeReportPage.tsx index 0bf7e3e..317cec8 100644 --- a/frontend/src/Pages/UserPages/UserEditTimeReportPage.tsx +++ b/frontend/src/Pages/UserPages/UserEditTimeReportPage.tsx @@ -5,6 +5,7 @@ import EditWeeklyReport from "../../Components/EditWeeklyReport"; function UserEditTimeReportPage(): JSX.Element { const content = ( <> +

    Edit Time Report

    ); diff --git a/frontend/src/Pages/UserPages/UserProjectPage.tsx b/frontend/src/Pages/UserPages/UserProjectPage.tsx index b9578b4..596c37d 100644 --- a/frontend/src/Pages/UserPages/UserProjectPage.tsx +++ b/frontend/src/Pages/UserPages/UserProjectPage.tsx @@ -1,11 +1,25 @@ +import { Link, useParams } from "react-router-dom"; import BasicWindow from "../../Components/BasicWindow"; import BackButton from "../../Components/BackButton"; -import UserProjectMenu from "../../Components/UserProjectMenu"; function UserProjectPage(): JSX.Element { + const { projectName } = useParams(); + const content = ( <> - +

    {projectName}

    +
    + +

    + Your Time Reports +

    + + +

    + New Time Report +

    + +
    ); diff --git a/frontend/src/Pages/YourProjectsPage.tsx b/frontend/src/Pages/YourProjectsPage.tsx index c048746..b5644c3 100644 --- a/frontend/src/Pages/YourProjectsPage.tsx +++ b/frontend/src/Pages/YourProjectsPage.tsx @@ -1,10 +1,39 @@ +import { useState, useEffect } from "react"; +import { Project } from "../Types/goTypes"; +import { Link } from "react-router-dom"; import BasicWindow from "../Components/BasicWindow"; -import DisplayUserProjects from "../Components/DisplayUserProjects"; +import { api } from "../API/API"; function UserProjectPage(): JSX.Element { + const [projects, setProjects] = useState([]); + + const getProjects = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUserProjects(token); + console.log(response); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error(response.message); + } + }; + // Call getProjects when the component mounts + useEffect(() => { + void getProjects(); + }, []); + const content = ( <> - +

    Your Projects

    +
    + {projects.map((project, index) => ( + +

    + {project.name} +

    + + ))} +
    ); diff --git a/frontend/src/Types/goTypes.ts b/frontend/src/Types/goTypes.ts index c519aac..89084b7 100644 --- a/frontend/src/Types/goTypes.ts +++ b/frontend/src/Types/goTypes.ts @@ -40,90 +40,6 @@ export interface NewWeeklyReport { */ testingTime: number /* int */; } -export interface WeeklyReportList { - /** - * The name of the project, as it appears in the database - */ - projectName: string; - /** - * The week number - */ - week: number /* int */; - /** - * Total time spent on development - */ - developmentTime: number /* int */; - /** - * Total time spent in meetings - */ - meetingTime: number /* int */; - /** - * Total time spent on administrative tasks - */ - adminTime: number /* int */; - /** - * Total time spent on personal projects - */ - ownWorkTime: number /* int */; - /** - * Total time spent on studying - */ - studyTime: number /* int */; - /** - * Total time spent on testing - */ - testingTime: number /* int */; - /** - * The project manager who signed it - */ - signedBy?: number /* int */; -} -export interface WeeklyReport { - /** - * The ID of the report - */ - reportId: number /* int */; - /** - * The user id of the user who submitted the report - */ - userId: number /* int */; - /** - * The name of the project, as it appears in the database - */ - projectId: number /* int */; - /** - * The week number - */ - week: number /* int */; - /** - * Total time spent on development - */ - developmentTime: number /* int */; - /** - * Total time spent in meetings - */ - meetingTime: number /* int */; - /** - * Total time spent on administrative tasks - */ - adminTime: number /* int */; - /** - * Total time spent on personal projects - */ - ownWorkTime: number /* int */; - /** - * Total time spent on studying - */ - studyTime: number /* int */; - /** - * Total time spent on testing - */ - testingTime: number /* int */; - /** - * The project manager who signed it - */ - signedBy?: number /* int */; -} ////////// // source: project.go @@ -144,27 +60,6 @@ export interface NewProject { name: string; description: string; } -/** - * Used to change the role of a user in a project. - * If name is identical to the name contained in the token, the role can be changed. - * If the name is different, only a project manager can change the role. - */ -export interface RoleChange { - username: string; - role: "project_manager" | "user"; - projectname: string; -} - -export interface NewProjMember { - username: string; - projectname: string; - role: string; -} - -export interface NameChange { - id: number /* int */; - name: string; -} ////////// // source: users.go @@ -191,18 +86,3 @@ export interface PublicUser { userId: string; username: string; } - -export interface UserProjectMember { - Username: string; - UserRole: string; -} -/** - * wrapper type for token - */ -export interface Token { - token: string; -} -export interface StrNameChange { - prevName: string; - newName: string; -} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index c26c033..9970cfa 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -30,8 +30,6 @@ import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.ts import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx"; import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx"; import NotFoundPage from "./Pages/NotFoundPage.tsx"; -import UnauthorizedPage from "./Pages/UnauthorizedPage.tsx"; -import PMViewOtherUsersTR from "./Pages/ProjectManagerPages/PMViewOtherUsersTR.tsx"; // This is where the routes are mounted const router = createBrowserRouter([ @@ -44,6 +42,10 @@ const router = createBrowserRouter([ path: "/admin", element: , }, + { + path: "/pm", + element: , + }, { path: "/yourProjects", element: , @@ -61,43 +63,39 @@ const router = createBrowserRouter([ element: , }, { - path: "/editTimeReport/:projectName/:fetchedWeek", + path: "/editTimeReport/:projectName/:weekNumber", element: , }, { - path: "/changeRole/:projectName/:username", + path: "/changeRole", element: , }, { - path: "/otherUsersTimeReports/:projectName/:username", + path: "/otherUsersTimeReports", element: , }, { - path: "/editOthersTR/:projectName/:username/:fetchedWeek", - element: , - }, - { - path: "/projectMembers/:projectName", + path: "/projectMembers", element: , }, { - path: "/PMProjectPage/:projectName", + path: "/PMProjectPage", element: , }, { - path: "/PMTimeActivity/:projectName", + path: "/PMTimeActivity", element: , }, { - path: "/PMTimeRole/:projectName", + path: "/PMTimeRole", element: , }, { - path: "/unsignedReports/:projectName", + path: "/PMUnsignedReports", element: , }, { - path: "/PMViewUnsignedReport/:projectName/:username/:fetchedWeek", + path: "/PMViewUnsignedReport", element: , }, { @@ -148,10 +146,6 @@ const router = createBrowserRouter([ path: "/adminManageUser", element: , }, - { - path: "/unauthorized", - element: , - }, ]); // Semi-hacky way to get the root element diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..087c85b --- /dev/null +++ b/go.work.sum @@ -0,0 +1,28 @@ +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5b28e18 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "TTime", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/testing.py b/testing.py index d4594d1..a3de715 100644 --- a/testing.py +++ b/testing.py @@ -2,7 +2,7 @@ import requests import string import random -debug_output = True +debug_output = False def gprint(*args, **kwargs): print("\033[92m", *args, "\033[00m", **kwargs) @@ -20,8 +20,8 @@ def randomString(len=10): # Defined once per test run -username = "user_" + randomString() -projectName = "project_" + randomString() +username = randomString() +projectName = randomString() # The base URL of the API base_url = "http://localhost:8080" @@ -37,50 +37,7 @@ signReportPath = base_url + "/api/signReport" addUserToProjectPath = base_url + "/api/addUserToProject" promoteToAdminPath = base_url + "/api/promoteToAdmin" getUserProjectsPath = base_url + "/api/getUserProjects" -getWeeklyReportsUserPath = base_url + "/api/getWeeklyReportsUser" -checkIfProjectManagerPath = base_url + "/api/checkIfProjectManager" -ProjectRoleChangePath = base_url + "/api/ProjectRoleChange" -getUsersProjectPath = base_url + "/api/getUsersProject" -getUnsignedReportsPath = base_url + "/api/getUnsignedReports" -getChangeUserNamePath = base_url + "/api/changeUserName" -getUpdateWeeklyReportPath = base_url + "/api/updateWeeklyReport" -removeProjectPath = base_url + "/api/removeProject" -#ta bort auth i handlern för att få testet att gå igenom -def test_ProjectRoleChange(): - dprint("Testing ProjectRoleChange") - localUsername = randomString() - localProjectName = randomString() - register(localUsername, "username_password") - - token = login(localUsername, "username_password").json()[ - "token" - ] - - # Just checking since this test is built somewhat differently than the others - assert token != None, "Login failed" - - response = requests.post( - addProjectPath, - json={"name": localProjectName, "description": "This is a project"}, - headers={"Authorization": "Bearer " + token}, - ) - - if response.status_code != 200: - print("Add project failed") - - response = requests.post( - ProjectRoleChangePath, - headers={"Authorization": "Bearer " + token}, - json={ - "projectName": localProjectName, - "role": "project_manager", - }, - ) - - assert response.status_code == 200, "ProjectRoleChange failed" - gprint("test_ProjectRoleChange successful") - def test_get_user_projects(): @@ -278,7 +235,7 @@ def test_sign_report(): submitReportPath, json={ "projectName": projectName, - "week": 2, + "week": 1, "developmentTime": 10, "meetingTime": 5, "adminTime": 5, @@ -301,8 +258,9 @@ def test_sign_report(): report_id = response.json()["reportId"] # Sign the report as the project manager - response = requests.put( - signReportPath + "/" + str(report_id), + response = requests.post( + signReportPath, + json={"reportId": report_id}, headers={"Authorization": "Bearer " + project_manager_token}, ) assert response.status_code == 200, "Sign report failed" @@ -317,206 +275,8 @@ def test_sign_report(): dprint(response.text) gprint("test_sign_report successful") -# Test function to get weekly reports for a user in a project -def test_get_weekly_reports_user(): - # Log in as the user - token = login(username, "always_same").json()["token"] - - # Get weekly reports for the user in the project - response = requests.get( - getWeeklyReportsUserPath + "/" + projectName, - headers={"Authorization": "Bearer " + token}, - ) - - dprint(response.text) - assert response.status_code == 200, "Get weekly reports for user failed" - gprint("test_get_weekly_reports_user successful") - - - -# Test function to check if a user is a project manager -def test_check_if_project_manager(): - # Log in as the user - token = login(username, "always_same").json()["token"] - - # Check if the user is a project manager for the project - response = requests.get( - checkIfProjectManagerPath + "/" + projectName, - headers={"Authorization": "Bearer " + token}, - ) - - dprint(response.text) - assert response.status_code == 200, "Check if project manager failed" - gprint("test_check_if_project_manager successful") - -def test_ensure_manager_of_created_project(): - # Create a new user to add to the project - newUser = "karen_" + randomString() - newProject = "HR_" + randomString() - register(newUser, "new_user_password") - token = login(newUser, "new_user_password").json()["token"] - - # Create a new project - response = requests.post( - addProjectPath, - json={"name": newProject, "description": "This is a project"}, - headers={"Authorization": "Bearer " + token}, - ) - assert response.status_code == 200, "Add project failed" - - response = requests.get( - checkIfProjectManagerPath + "/" + newProject, - headers={"Authorization": "Bearer " + token}, - ) - assert response.status_code == 200, "Check if project manager failed" - assert response.json()["isProjectManager"] == True, "User is not project manager" - gprint("test_ensure_admin_of_created_project successful") - -def test_change_user_name(): - # Register a new user - new_user = randomString() - register(new_user, "password") - - # Log in as the new user - token = login(new_user, "password").json()["token"] - - # Register a new admin - admin_username = randomString() - admin_password = "admin_password" - dprint( - "Registering with username: ", admin_username, " and password: ", admin_password - ) - response = requests.post( - registerPath, json={"username": admin_username, "password": admin_password} - ) - admin_token = login(admin_username, admin_password).json()["token"] - - # Promote to admin - response = requests.post( - promoteToAdminPath, - json={"username": admin_username}, - headers={"Authorization": "Bearer " + admin_token}, - ) - - # Change the user's name - response = requests.put( - getChangeUserNamePath, - json={"prevName": new_user, "newName": randomString()}, - headers={"Authorization": "Bearer " + admin_token}, - ) - - # Check if the change was successful - assert response.status_code == 200, "Change user name failed" - gprint("test_change_user_name successful") - -def test_list_all_users_project(): - # Log in as a user who is a member of the project - admin_username = randomString() - admin_password = "admin_password2" - dprint( - "Registering with username: ", admin_username, " and password: ", admin_password - ) - response = requests.post( - registerPath, json={"username": admin_username, "password": admin_password} - ) - dprint(response.text) - - # Log in as the admin - admin_token = login(admin_username, admin_password).json()["token"] - response = requests.post( - promoteToAdminPath, - json={"username": admin_username}, - headers={"Authorization": "Bearer " + admin_token}, - ) - - # Make a request to list all users associated with the project - response = requests.get( - getUsersProjectPath + "/" + projectName, - headers={"Authorization": "Bearer " + admin_token}, - ) - assert response.status_code == 200, "List all users project failed" - gprint("test_list_all_users_project sucessful") - -def test_update_weekly_report(): - # Log in as the user - token = login(username, "always_same").json()["token"] - - # Prepare the JSON data for updating the weekly report - update_data = { - "projectName": projectName, - "userName": username, - "week": 1, - "developmentTime": 8, - "meetingTime": 6, - "adminTime": 4, - "ownWorkTime": 11, - "studyTime": 8, - "testingTime": 18, - } - - # Send a request to update the weekly report - response = requests.put( - getUpdateWeeklyReportPath, - json=update_data, - headers={"Authorization": "Bearer " + token}, - ) - - # Check if the update was successful - assert response.status_code == 200, "Update weekly report failed" - gprint("test_update_weekly_report successful") - - -def test_remove_project(): - admin_username = randomString() - admin_password = "admin_password2" - dprint( - "Registering with username: ", admin_username, " and password: ", admin_password - ) - response = requests.post( - registerPath, json={"username": admin_username, "password": admin_password} - ) - dprint(response.text) - - # Log in as the admin - admin_token = login(admin_username, admin_password).json()["token"] - response = requests.post( - promoteToAdminPath, - json={"username": admin_username}, - headers={"Authorization": "Bearer " + admin_token}, - ) - - # Create a new project - new_project = randomString() - response = requests.post( - addProjectPath, - json={"name": new_project, "description": "This is a project"}, - headers={"Authorization": "Bearer " + admin_token}, - ) - assert response.status_code == 200, "Add project failed" - - # Remove the project - response = requests.delete( - removeProjectPath + "/" + new_project, - headers={"Authorization": "Bearer " + admin_token}, - ) - assert response.status_code == 200, "Remove project failed" - gprint("test_remove_project successful") - -def test_get_unsigned_reports(): - # Log in as the user - token = login("user2", "123").json()["token"] - - # Make a request to get all unsigned reports - response = requests.get( - getUnsignedReportsPath + "/" + projectName, - headers={"Authorization": "Bearer " + token}, - ) - assert response.status_code == 200, "Get unsigned reports failed" - gprint("test_get_unsigned_reports successful") - if __name__ == "__main__": - test_remove_project() test_get_user_projects() test_create_user() test_login() @@ -526,12 +286,3 @@ if __name__ == "__main__": test_get_project() test_sign_report() test_add_user_to_project() - test_get_weekly_reports_user() - test_check_if_project_manager() - test_ProjectRoleChange() - test_ensure_manager_of_created_project() - test_get_unsigned_reports() - test_list_all_users_project() - test_change_user_name() - test_update_weekly_report() - \ No newline at end of file