diff --git a/Makefile b/Makefile index 97db62e..668ccf1 100644 --- a/Makefile +++ b/Makefile @@ -27,10 +27,6 @@ clean: remove-podman-containers cd backend && make clean @echo "Cleaned up!" -.PHONY: itest -itest: - python testing.py - # Cleans up everything related to podman, not just the project. Make sure you understand what this means. podman-clean: podman system reset --force diff --git a/backend/Makefile b/backend/Makefile index da0e254..9cfa335 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -118,7 +118,3 @@ uml: plantuml.jar install-just: @echo "Installing just" @curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin - -.PHONY: types -types: - tygo generate \ No newline at end of file diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5cbb13f..b5e1981 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -2,8 +2,9 @@ package database import ( "embed" - "errors" + "os" "path/filepath" + "time" "ttime/internal/types" "github.com/jmoiron/sqlx" @@ -14,24 +15,20 @@ import ( type Database interface { // Insert a new user into the database, password should be hashed before calling AddUser(username string, password string) error - CheckUser(username string, password string) bool RemoveUser(username string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error - Migrate() error + Migrate(dirname string) 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 + AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error AddUserToProject(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) GetAllProjects() ([]types.Project, error) - GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) - GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) - SignWeeklyReport(reportId int, projectManagerId int) error } // This struct is a wrapper type that holds the database connection @@ -52,16 +49,27 @@ var scripts embed.FS const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" 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 = ?), +const addTimeReport = `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),?, ?, ?, ?, ?, ?, ?);` + INSERT INTO time_reports (project_id, user_id, start, end) + VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup), ?, ?);` const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" -const getProjectsForUser = `SELECT projects.id, projects.name, projects.description, projects.owner_user_id - FROM projects JOIN user_roles ON projects.id = user_roles.project_id - JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;` +const getProjectsForUser = ` +SELECT + projects.id, + projects.name, + projects.description, + projects.owner_user_id +FROM + projects +JOIN + user_roles ON projects.id = user_roles.project_id +JOIN + users ON user_roles.user_id = users.id +WHERE + users.username = ?;` // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -80,42 +88,23 @@ func DbConnect(dbpath string) Database { return &Db{db} } -func (d *Db) CheckUser(username string, password string) bool { - var dbPassword string - err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username) - if err != nil { - return false - } - return dbPassword == password -} - -// GetProjectsForUser retrieves all projects associated with a specific user. func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, getProjectsForUser, username) return projects, err } -// GetAllProjects retrieves all projects from the database. func (d *Db) GetAllProjects() ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, "SELECT * FROM projects") return projects, err } -// GetProject retrieves a specific project by its ID. -func (d *Db) GetProject(projectId int) (types.Project, error) { - var project types.Project - err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") - return project, err -} - -func (d *Db) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { - _, err := d.Exec(addWeeklyReport, userName, projectName, week, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime) +func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP + _, err := d.Exec(addTimeReport, userName, projectName, start, end) return err } -// AddUserToProject adds a user to a project with a specified role. func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP var userid int userid, err := d.GetUserId(username) @@ -133,28 +122,23 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) return err3 } -// ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { - // Get the user ID var userid int userid, err := d.GetUserId(username) if err != nil { panic(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. func (d *Db) GetUserRole(username string, projectname string) (string, error) { var role string err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname) @@ -259,74 +243,15 @@ func (d *Db) GetAllUsersApplication() ([]string, error) { return usernames, nil } -func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) { - var report types.WeeklyReport - 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 - user_id = (SELECT id FROM users WHERE username = ?) - AND project_id = (SELECT id FROM projects WHERE name = ?) - AND week = ? - ` - err := d.Get(&report, query, username, projectName, week) - return report, err -} - -// SignWeeklyReport signs a weekly report by updating the signed_by field -// with the provided project manager's ID, but only if the project manager -// is in the same project as the report -func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { - // Retrieve the project ID associated with the report - var reportProjectID int - err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId) - if err != nil { - return err - } - - // Retrieve the project ID associated with the project manager - var managerProjectID int - err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId) - if err != nil { - return err - } - - // Check if the project manager is in the same project as the report - if reportProjectID != managerProjectID { - return errors.New("project manager doesn't have permission to sign the report") - } - - // Update the signed_by field of the specified report - _, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId) - return err -} - // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory -func (d *Db) Migrate() error { +func (d *Db) Migrate(dirname string) error { // Read the embedded scripts directory files, err := scripts.ReadDir("migrations") if err != nil { return err } - if len(files) == 0 { - println("No migration files found") - return nil - } - tr := d.MustBegin() // Iterate over each SQL file and execute it @@ -336,7 +261,8 @@ func (d *Db) Migrate() error { } // This is perhaps not the most elegant way to do this - sqlBytes, err := scripts.ReadFile("migrations/" + file.Name()) + sqlFile := filepath.Join("migrations", file.Name()) + sqlBytes, err := os.ReadFile(sqlFile) if err != nil { return err } diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 09de45b..7650739 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -1,15 +1,15 @@ package database import ( - "fmt" "testing" + "time" ) // Tests are not guaranteed to be sequential func setupState() (Database, error) { db := DbConnect(":memory:") - err := db.Migrate() + err := db.Migrate("../../migrations") if err != nil { return nil, err } @@ -93,7 +93,7 @@ func TestPromoteToAdmin(t *testing.T) { } } -func TestAddWeeklyReport(t *testing.T) { +func TestAddTimeReport(t *testing.T) { db, err := setupState() if err != nil { t.Error("setupState failed:", err) @@ -109,9 +109,12 @@ func TestAddWeeklyReport(t *testing.T) { t.Error("AddProject failed:", err) } - err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + var now = time.Now() + var then = now.Add(time.Hour) + + err = db.AddTimeReport("testproject", "testuser", now, then) if err != nil { - t.Error("AddWeeklyReport failed:", err) + t.Error("AddTimeReport failed:", err) } } @@ -131,9 +134,12 @@ func TestAddUserToProject(t *testing.T) { t.Error("AddProject failed:", err) } - err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) + var now = time.Now() + var then = now.Add(time.Hour) + + err = db.AddTimeReport("testproject", "testuser", now, then) if err != nil { - t.Error("AddWeeklyReport failed:", err) + t.Error("AddTimeReport failed:", err) } err = db.AddUserToProject("testuser", "testproject", "user") @@ -337,202 +343,3 @@ func TestGetProjectsForUser(t *testing.T) { t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) } } - -func TestAddProject(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) - } - - // Retrieve the added project to verify its existence - projects, err := db.GetAllProjects() - if err != nil { - t.Error("GetAllProjects failed:", err) - } - - // Check if the project was added successfully - found := false - for _, project := range projects { - if project.Name == "testproject" { - found = true - break - } - } - if !found { - t.Error("Added project not found") - } -} - -func TestGetWeeklyReport(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) - } - - report, err := db.GetWeeklyReport("testuser", "testproject", 1) - if err != nil { - t.Error("GetWeeklyReport failed:", err) - } - - // Check if the retrieved report matches the expected values - if report.UserId != 1 { - t.Errorf("Expected UserId to be 1, got %d", report.UserId) - } - if report.ProjectId != 1 { - t.Errorf("Expected ProjectId to be 1, got %d", report.ProjectId) - } - if report.Week != 1 { - t.Errorf("Expected Week to be 1, got %d", report.Week) - } - // Check other fields similarly -} - -func TestSignWeeklyReport(t *testing.T) { - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - } - - // Add 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) - } - - // Add a weekly report for one of the regular users - err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) - if err != nil { - t.Error("AddWeeklyReport failed:", err) - } - - // Retrieve the added report - report, err := db.GetWeeklyReport("testuser", "testproject", 1) - if err != nil { - t.Error("GetWeeklyReport failed:", err) - } - - // Print project manager's ID - projectManagerID, err := db.GetUserId("projectManager") - 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) - 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 != projectManagerID { - t.Errorf("Expected SignedBy to be %d, got %d", projectManagerID, *signedReport.SignedBy) - } -} - -func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) { - db, err := setupState() - if err != nil { - t.Error("setupState failed:", err) - } - - // Add 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 the regular user as a member to the project - err = db.AddUserToProject("testuser", "testproject", "member") - if err != nil { - t.Error("AddUserToProject failed:", err) - } - - // Add a weekly report for the regular user - err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) - if err != nil { - t.Error("AddWeeklyReport failed:", err) - } - - // Retrieve the added report - report, err := db.GetWeeklyReport("testuser", "testproject", 1) - if err != nil { - t.Error("GetWeeklyReport failed:", err) - } - - anotherManagerID, err := db.GetUserId("projectManager") - if err != nil { - t.Error("GetUserId failed:", err) - } - - 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") - } -} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index d2e2dd1..5c9d329 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -3,7 +3,7 @@ -- username is what is used for login -- password is the hashed password CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER PRIMARY KEY, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index 99bce86..58d8e97 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -1,5 +1,5 @@ CREATE TABLE IF NOT EXISTS projects ( - id INTEGER PRIMARY KEY AUTOINCREMENT, + id INTEGER PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, owner_user_id INTEGER NOT NULL, diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql new file mode 100644 index 0000000..76812a1 --- /dev/null +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -0,0 +1,20 @@ +CREATE TABLE IF NOT EXISTS time_reports ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + start DATETIME NOT NULL, + end DATETIME NOT NULL, + FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end + BEFORE INSERT ON time_reports + FOR EACH ROW + BEGIN + SELECT + CASE + WHEN NEW.start >= NEW.end THEN + RAISE (ABORT, 'start must be before end') + END; + END; \ No newline at end of file diff --git a/backend/internal/database/migrations/0035_weekly_report.sql b/backend/internal/database/migrations/0035_weekly_report.sql deleted file mode 100644 index 8f76b80..0000000 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE IF NOT EXISTS weekly_reports ( - report_id INTEGER PRIMARY KEY AUTOINCREMENT, - user_id INTEGER NOT NULL, - project_id INTEGER NOT NULL, - week INTEGER NOT NULL, - development_time INTEGER, - meeting_time INTEGER, - admin_time INTEGER, - own_work_time INTEGER, - study_time INTEGER, - testing_time INTEGER, - signed_by INTEGER, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (project_id) REFERENCES projects(id), - FOREIGN KEY (signed_by) REFERENCES users(id) -); \ No newline at end of file diff --git a/backend/internal/database/migrations/0040_time_report_collections.sql b/backend/internal/database/migrations/0040_time_report_collections.sql new file mode 100644 index 0000000..be406ff --- /dev/null +++ b/backend/internal/database/migrations/0040_time_report_collections.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS report_collection ( + id INTEGER PRIMARY KEY, + owner_id INTEGER NOT NULL, + project_id INTEGER NOT NULL, + date DATE NOT NULL, + signed_by INTEGER, -- NULL if not signed + FOREIGN KEY (owner_id) REFERENCES users (id) + FOREIGN KEY (signed_by) REFERENCES users (id) +); \ No newline at end of file diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql new file mode 100644 index 0000000..b84dfac --- /dev/null +++ b/backend/internal/database/migrations/0070_salts.sql @@ -0,0 +1,16 @@ +-- It is unclear weather this table will be used + +-- Create the table to store hash salts +CREATE TABLE salts ( + id INTEGER PRIMARY KEY, + salt TEXT NOT NULL +); + +-- Commented out for now, no time for good practices, which is atrocious +-- Create a trigger to automatically generate a salt when inserting a new user record +-- CREATE TRIGGER generate_salt_trigger +-- AFTER INSERT ON users +-- BEGIN +-- INSERT INTO salts (salt) VALUES (randomblob(16)); +-- UPDATE users SET salt_id = (SELECT last_insert_rowid()) WHERE id = new.id; +-- END; diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index c8beb1c..fea0dfd 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,9 +1,12 @@ package handlers import ( + "time" "ttime/internal/database" + "ttime/internal/types" "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v5" ) // The actual interface that we will use @@ -14,9 +17,6 @@ type GlobalState interface { 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 // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project // DeleteProject(c *fiber.Ctx) error // To delete a project @@ -49,6 +49,45 @@ type GState struct { ButtonCount int } +// Register is a simple handler that registers a new user +// +// @Summary Register a new user +// @Description Register a new user +// @Tags User +// @Accept json +// @Produce json +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /api/register [post] +func (gs *GState) Register(c *fiber.Ctx) error { + u := new(types.NewUser) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) + } + + if err := gs.Db.AddUser(u.Username, u.Password); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User added") +} + +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +func (gs *GState) UserDelete(c *fiber.Ctx) error { + u := new(types.User) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) + } + + if err := gs.Db.RemoveUser(u.Username); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("User deleted") +} + func (gs *GState) GetButtonCount(c *fiber.Ctx) error { return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) } @@ -57,3 +96,132 @@ func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { gs.ButtonCount++ return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) } + +// Login is a simple login handler that returns a JWT token +func (gs *GState) Login(c *fiber.Ctx) error { + // To test: curl --data "user=user&pass=pass" http://localhost:8080/api/login + user := c.FormValue("user") + pass := c.FormValue("pass") + + // Throws Unauthorized error + if user != "user" || pass != "pass" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + // Create the Claims + claims := jwt.MapClaims{ + "name": user, + "admin": false, + "exp": time.Now().Add(time.Hour * 72).Unix(), + } + + // Create token + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + // Generate encoded token and send it as response. + t, err := token.SignedString([]byte("secret")) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + return c.JSON(fiber.Map{"token": t}) +} + +// LoginRenew is a simple handler that renews the token +func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " + user := c.Locals("user").(*jwt.Token) + 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 { + return c.SendStatus(fiber.StatusInternalServerError) + } + return c.JSON(fiber.Map{"token": t}) +} + +// 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) + p.Owner = claims["name"].(string) + + if err := gs.Db.AddProject(p.Name, p.Description, p.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) +} + +// ListAllUsers is a handler that returns a list of all users in the application database +func (gs *GState) ListAllUsers(c *fiber.Ctx) error { + // Get all users from the database + users, err := gs.Db.GetAllUsersApplication() + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { + // Extract the project name from the request parameters or body + projectName := c.Params("projectName") + + // Get all users associated with the project from the database + users, err := gs.Db.GetAllUsersProject(projectName) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return the list of users as JSON + return c.JSON(users) +} + +// 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) +} diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go deleted file mode 100644 index 6a430e9..0000000 --- a/backend/internal/handlers/handlers_project_related.go +++ /dev/null @@ -1,98 +0,0 @@ -package handlers - -import ( - "strconv" - "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 (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") - - // Parse the project ID into an integer - projectIDInt, err := strconv.Atoi(projectID) - if err != nil { - 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 { - return c.Status(500).SendString(err.Error()) - } - - // Return the project as JSON - 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") - - // Get all users associated with the project from the database - users, err := gs.Db.GetAllUsersProject(projectName) - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go deleted file mode 100644 index 509bd67..0000000 --- a/backend/internal/handlers/handlers_report_related.go +++ /dev/null @@ -1,93 +0,0 @@ -package handlers - -import ( - "strconv" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "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 { - return c.Status(400).SendString(err.Error()) - } - - // Make sure all the fields of the report are valid - if report.Week < 1 || report.Week > 52 { - 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 { - 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 { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("Time report added") -} - -// Handler for retrieving weekly report -func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the request - println("GetWeeklyReport") - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) - - // Extract project name and week from query parameters - projectName := c.Query("projectName") - println(projectName) - week := c.Query("week") - println(week) - - // Convert week to integer - weekInt, err := strconv.Atoi(week) - if err != nil { - 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 { - return c.Status(500).SendString(err.Error()) - } - - // Return the retrieved weekly report - return c.JSON(report) -} - -func (gs *GState) SignReport(c *fiber.Ctx) error { - // Extract the necessary parameters from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - managerUsername := claims["name"].(string) - - // Extract the report ID and project manager ID from request parameters - reportID, err := strconv.Atoi(c.Params("reportId")) - if err != nil { - return c.Status(400).SendString("Invalid report ID") - } - - // Call the database function to get the project manager ID - managerID, err := gs.Db.GetUserId(managerUsername) - if err != nil { - return c.Status(500).SendString("Failed to get project manager ID") - } - - // Call the database function to sign the weekly report - err = gs.Db.SignWeeklyReport(reportID, managerID) - if err != nil { - return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error()) - } - - // Return success response - return c.Status(200).SendString("Weekly report signed successfully") -} diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go deleted file mode 100644 index 0619ea5..0000000 --- a/backend/internal/handlers/handlers_user_related.go +++ /dev/null @@ -1,124 +0,0 @@ -package handlers - -import ( - "time" - "ttime/internal/types" - - "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" -) - -// Register is a simple handler that registers a new user -// -// @Summary Register a new user -// @Description Register a new user -// @Tags User -// @Accept json -// @Produce json -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /api/register [post] -func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - println("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - println("Adding user:", u.Username) - if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - return c.Status(500).SendString(err.Error()) - } - - println("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 -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 { - return c.Status(403).SendString("You can only delete yourself") - } - - if err := gs.Db.RemoveUser(username); err != nil { - return c.Status(500).SendString(err.Error()) - } - - return c.Status(200).SendString("User deleted") -} - -// Login is a simple login handler that returns a JWT token -func (gs *GState) Login(c *fiber.Ctx) error { - // The body type is identical to a NewUser - u := new(types.NewUser) - if err := c.BodyParser(u); err != nil { - println("Error parsing body") - return c.Status(400).SendString(err.Error()) - } - - println("Username:", u.Username) - if !gs.Db.CheckUser(u.Username, u.Password) { - println("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) - println("Token created for user:", u.Username) - - // Generate encoded token and send it as response. - t, err := token.SignedString([]byte("secret")) - if err != nil { - println("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) - } - - println("Successfully signed token for user:", u.Username) - return c.JSON(fiber.Map{"token": t}) -} - -// LoginRenew is a simple handler that renews the token -func (gs *GState) LoginRenew(c *fiber.Ctx) error { - // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " - user := c.Locals("user").(*jwt.Token) - 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 { - return c.SendStatus(fiber.StatusInternalServerError) - } - return c.JSON(fiber.Map{"token": t}) -} - -// ListAllUsers is a handler that returns a list of all users in the application database -func (gs *GState) ListAllUsers(c *fiber.Ctx) error { - // Get all users from the database - users, err := gs.Db.GetAllUsersApplication() - if err != nil { - return c.Status(500).SendString(err.Error()) - } - - // Return the list of users as JSON - return c.JSON(users) -} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go deleted file mode 100644 index 299395a..0000000 --- a/backend/internal/types/WeeklyReport.go +++ /dev/null @@ -1,46 +0,0 @@ -package types - -// This is what should be submitted to the server, the username will be derived from the JWT token -type NewWeeklyReport struct { - // The name of the project, as it appears in the database - ProjectName string `json:"projectName"` - // 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"` -} - -type WeeklyReport struct { - // The ID of the report - ReportId int `json:"reportId" db:"report_id"` - // The user id of the user who submitted the report - UserId int `json:"userId" db:"user_id"` - // The name of the project, as it appears in the database - ProjectId int `json:"projectId" db:"project_id"` - // 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"` -} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go index 7e1747f..8fcfaf5 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -8,8 +8,9 @@ type Project struct { Owner string `json:"owner" db:"owner_user_id"` } -// As it arrives from the client, Owner is derived from the JWT token +// As it arrives from the client type NewProject struct { Name string `json:"name"` Description string `json:"description"` + Owner string `json:"owner"` } diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index e9dff67..233ec71 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -16,7 +16,6 @@ func (u *User) ToPublicUser() (*PublicUser, error) { }, nil } -// Should be used when registering, for example type NewUser struct { Username string `json:"username"` Password string `json:"password"` diff --git a/backend/main.go b/backend/main.go index bc33942..4e0935c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -43,11 +43,6 @@ 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) - } - // Get our global state gs := handlers.NewGlobalState(db) // Create the server @@ -73,12 +68,10 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) - server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) - server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches + server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) - server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) diff --git a/backend/tygo.yaml b/backend/tygo.yaml deleted file mode 100644 index 54c1e8f..0000000 --- a/backend/tygo.yaml +++ /dev/null @@ -1,9 +0,0 @@ -packages: - - path: "ttime/internal/types" - output_path: "../frontend/src/Types/goTypes.ts" - type_mappings: - time.Time: "string /* RFC3339 */" - null.String: "null | string" - null.Bool: "null | boolean" - uuid.UUID: "string /* uuid */" - uuid.NullUUID: "null | string /* uuid */" diff --git a/frontend/.eslintrc.cjs b/frontend/.eslintrc.cjs index 1051c03..447a464 100644 --- a/frontend/.eslintrc.cjs +++ b/frontend/.eslintrc.cjs @@ -9,7 +9,7 @@ module.exports = { 'plugin:react-hooks/recommended', 'plugin:prettier/recommended', ], - ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs', 'goTypes.ts'], + ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs'], parser: '@typescript-eslint/parser', plugins: ['react-refresh', 'prettier'], rules: { diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 7a1ccd0..f33c87c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,256 +1,57 @@ -import { - NewWeeklyReport, - NewUser, - User, - Project, - NewProject, -} from "../Types/goTypes"; +import { NewProject, Project } from "../Types/Project"; +import { NewUser, User } from "../Types/Users"; -// This type of pattern should be hard to misuse -export interface APIResponse { - success: boolean; - message?: string; - data?: T; -} - -// 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 */ - registerUser(user: NewUser): Promise>; + registerUser(user: NewUser): Promise; /** Remove a user */ - removeUser(username: string, token: string): Promise>; - /** Login */ - login(NewUser: NewUser): Promise>; - /** Renew the token */ - renewToken(token: string): Promise>; + removeUser(username: string): Promise; /** Create a project */ - createProject( - project: NewProject, - token: string, - ): Promise>; - /** Gets all the projects of a user*/ - getUserProjects( - username: string, - token: string, - ): Promise>; - /** Submit a weekly report */ - submitWeeklyReport( - project: NewWeeklyReport, - token: string, - ): Promise>; - /**Gets a weekly report*/ - getWeeklyReport( - username: string, - projectName: string, - week: string, - token: string, - ): Promise>; + createProject(project: NewProject): Promise; + /** Renew the token */ + renewToken(token: string): Promise; } // Export an instance of the API export const api: API = { - async registerUser(user: NewUser): Promise> { - try { - const response = await fetch("/api/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(user), - }); - - if (!response.ok) { - return { - success: false, - message: "Failed to register user: " + response.status, - }; - } else { - // const data = (await response.json()) as User; // The API does not currently return the user - return { success: true }; - } - } catch (e) { - return { - success: false, - message: "Unknown error while registering user", - }; - } + async registerUser(user: NewUser): Promise { + return fetch("/api/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(user), + }).then((res) => res.json() as Promise); }, - async removeUser( - username: string, - token: string, - ): Promise> { - try { - 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: "Failed to remove user" }; - } else { - const data = (await response.json()) as User; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to remove user" }; - } + async removeUser(username: string): Promise { + return fetch("/api/userdelete", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(username), + }).then((res) => res.json() as Promise); }, - async createProject( - project: NewProject, - token: string, - ): Promise> { - try { - const response = await fetch("/api/project", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(project), - }); - - if (!response.ok) { - return { success: false, message: "Failed to create project" }; - } else { - const data = (await response.json()) as Project; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to create project" }; - } + async createProject(project: NewProject): Promise { + return fetch("/api/project", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(project), + }).then((res) => res.json() as Promise); }, - async renewToken(token: string): Promise> { - try { - const response = await fetch("/api/loginrenew", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }); - - if (!response.ok) { - return { success: false, message: "Failed to renew token" }; - } else { - const data = (await response.json()) as string; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to renew token" }; - } - }, - - async getUserProjects(token: string): Promise> { - try { - const response = await fetch("/api/getUserProjects", { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }); - - if (!response.ok) { - return Promise.resolve({ - success: false, - message: "Failed to get user projects", - }); - } else { - const data = (await response.json()) as Project[]; - return Promise.resolve({ success: true, data }); - } - } catch (e) { - return Promise.resolve({ - success: false, - message: "Failed to get user projects", - }); - } - }, - - async submitWeeklyReport( - weeklyReport: NewWeeklyReport, - token: string, - ): Promise> { - try { - const response = await fetch("/api/submitWeeklyReport", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(weeklyReport), - }); - - if (!response.ok) { - return { - success: false, - message: "Failed to submit weekly report", - }; - } - - const data = (await response.json()) as NewWeeklyReport; - return { success: true, data }; - } catch (e) { - return { - success: false, - message: "Failed to submit weekly report", - }; - } - }, - - async getWeeklyReport( - username: string, - projectName: string, - week: string, - token: string, - ): Promise> { - try { - 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 report" }; - } else { - const data = (await response.json()) as NewWeeklyReport; - return { success: true, data }; - } - } catch (e) { - return { success: false, message: "Failed to get weekly report" }; - } - }, - - async login(NewUser: NewUser): Promise> { - try { - const response = await fetch("/api/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(NewUser), - }); - - if (!response.ok) { - return { success: false, message: "Failed to login" }; - } else { - const data = (await response.json()) as { token: string }; // Update the type of 'data' - return { success: true, data: data.token }; - } - } catch (e) { - return Promise.resolve({ success: false, message: "Failed to login" }); - } + async renewToken(token: string): Promise { + return fetch("/api/loginrenew", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token, + }, + }).then((res) => res.json() as Promise); }, }; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx deleted file mode 100644 index 45814e3..0000000 --- a/frontend/src/Components/AddProject.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from "react"; -import { APIResponse, api } from "../API/API"; -import { NewProject, Project } from "../Types/goTypes"; -import InputField from "./InputField"; -import Logo from "../assets/Logo.svg"; -import Button from "./Button"; - -/** - * Tries to add a project to the system - * @param props - Project name and description - * @returns {boolean} True if created, false if not - */ -function CreateProject(props: { name: string; description: string }): boolean { - const project: NewProject = { - name: props.name, - description: props.description, - }; - - let created = false; - - api - .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - created = true; - } else { - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during creation:", error); - }); - return created; -} - -/** - * Tries to add a project to the system - * @returns {JSX.Element} UI for project adding - */ -function AddProject(): JSX.Element { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - - return ( -
-
-
{ - e.preventDefault(); - CreateProject({ name: name, description: description }); - }} - > - TTIME Logo -

- Create a new project -

- { - setName(e.target.value); - }} - /> - { - setDescription(e.target.value); - }} - /> -
-
- -

-
-
- ); -} - -export default AddProject; diff --git a/frontend/src/Components/BackgroundAnimation.tsx b/frontend/src/Components/BackgroundAnimation.tsx deleted file mode 100644 index 5f402c0..0000000 --- a/frontend/src/Components/BackgroundAnimation.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { useEffect } from "react"; - -const BackgroundAnimation = (): JSX.Element => { - useEffect(() => { - const images = [ - "src/assets/1.jpg", - "src/assets/2.jpg", - "src/assets/3.jpg", - "src/assets/4.jpg", - ]; - - // Pre-load images - for (const i of images) { - console.log(i); - } - - // Start animation - document.body.style.animation = "backgroundTransition 30s infinite"; - }, []); - - return <>; -}; - -export default BackgroundAnimation; diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx index d5fd3b6..1835d6a 100644 --- a/frontend/src/Components/BasicWindow.tsx +++ b/frontend/src/Components/BasicWindow.tsx @@ -2,15 +2,17 @@ import Header from "./Header"; import Footer from "./Footer"; function BasicWindow({ + username, content, buttons, }: { + username: string; content: React.ReactNode; buttons: React.ReactNode; }): JSX.Element { return (
-
+
{content}
{buttons}
diff --git a/frontend/src/Components/Button.tsx b/frontend/src/Components/Button.tsx index 38a1853..cf6a887 100644 --- a/frontend/src/Components/Button.tsx +++ b/frontend/src/Components/Button.tsx @@ -1,17 +1,14 @@ function Button({ text, onClick, - type, }: { text: string; onClick: () => void; - type: "submit" | "button" | "reset"; }): JSX.Element { return ( diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx deleted file mode 100644 index 9321d73..0000000 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { useState, useEffect } from "react"; -import { NewWeeklyReport } from "../Types/goTypes"; -import { api } from "../API/API"; -import { useNavigate } from "react-router-dom"; -import Button from "./Button"; - -export default function GetWeeklyReport(): JSX.Element { - const [projectName, setProjectName] = useState(""); - 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 username = localStorage.getItem("username") ?? ""; - - useEffect(() => { - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - username, - projectName, - week.toString(), - token, - ); - - if (response.success) { - const report: NewWeeklyReport = response.data ?? { - projectName: "", - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setProjectName(report.projectName); - 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); - } - }; - - fetchWeeklyReport(); - }, []); - - const handleNewWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { - projectName, - week, - developmentTime, - meetingTime, - adminTime, - ownWorkTime, - studyTime, - testingTime, - }; - - await api.submitWeeklyReport(newWeeklyReport, token); - }; - - const navigate = useNavigate(); - - return ( - <> -
-
{ - if (week === 0) { - alert("Please enter a week number"); - e.preventDefault(); - return; - } - e.preventDefault(); - void handleNewWeeklyReport(); - navigate("/project"); - }} - > -
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Activity - - Total Time (min) -
Development - { - setDevelopmentTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Meeting - { - setMeetingTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Administration - { - setAdminTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Own Work - { - setOwnWorkTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Studies - { - setStudyTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Testing - { - setTestingTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
-
-
-
- - ); -} diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index 819c5de..ba0a939 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -function Header(): JSX.Element { +function Header({ username }: { username: string }): JSX.Element { const [isOpen, setIsOpen] = useState(false); const handleLogout = (): void => { - localStorage.clear(); + // Add any logout logic here }; return ( @@ -31,7 +31,7 @@ function Header(): JSX.Element { }} > {isOpen && ( diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx deleted file mode 100644 index 639b4ca..0000000 --- a/frontend/src/Components/InputField.tsx +++ /dev/null @@ -1,41 +0,0 @@ -/** - * A customizable input field - * @param props - Settings for the field - * @returns {JSX.Element} The input field - * @example - * { - * setExample(e.target.value); - * }} - * value={example} - * /> - */ -function InputField(props: { - label: string; - type: string; - value: string; - onChange: (e: React.ChangeEvent) => void; -}): JSX.Element { - return ( -
- - -
- ); -} - -export default InputField; diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx deleted file mode 100644 index ce7d52c..0000000 --- a/frontend/src/Components/LoginCheck.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { NewUser } from "../Types/goTypes"; -import { api, APIResponse } from "../API/API"; -import { Dispatch, SetStateAction } from "react"; - -/* - * Checks if user is in database with api.login and then sets proper authority level - * TODO: change so that it checks for user type (admin, user, pm) somehow instead - **/ -function LoginCheck(props: { - username: string; - password: string; - setAuthority: Dispatch>; -}): void { - const user: NewUser = { - username: props.username, - password: props.password, - }; - - localStorage.clear(); - - api - .login(user) - .then((response: APIResponse) => { - if (response.success) { - if (response.data !== undefined) { - const token = response.data; - localStorage.setItem("accessToken", token); - localStorage.setItem("username", props.username); - //TODO: change so that it checks for user type (admin, user, pm) instead - if (token !== "" && props.username === "admin") { - props.setAuthority((prevAuth) => { - prevAuth = 1; - return prevAuth; - }); - } 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"); - } - } else { - console.error("Token could not be fetched/No such user"); - } - }) - .catch((error) => { - console.error("An error occurred during login:", error); - }); -} - -export default LoginCheck; diff --git a/frontend/src/Components/LoginField.tsx b/frontend/src/Components/LoginField.tsx deleted file mode 100644 index d7768c0..0000000 --- a/frontend/src/Components/LoginField.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Dispatch, FormEventHandler, SetStateAction } from "react"; -import Button from "./Button"; -import InputField from "./InputField"; - -/** - * A login field complete with input fields - * and a button for submitting the information - * @param props - Settings - * @returns {JSX.Element} A login component - * @example - * - */ -function Login(props: { - handleSubmit: FormEventHandler; - setUsername: Dispatch>; - setPassword: Dispatch>; - username: string; - password: string; -}): JSX.Element { - return ( -
- { - props.setUsername(e.target.value); - }} - value={props.username} - /> - { - props.setPassword(e.target.value); - }} - value={props.password} - /> - +

diff --git a/frontend/src/Components/TimeReport.tsx b/frontend/src/Components/TimeReport.tsx new file mode 100644 index 0000000..c4ddc38 --- /dev/null +++ b/frontend/src/Components/TimeReport.tsx @@ -0,0 +1,59 @@ +function NewTimeReport(): JSX.Element { + const activities = [ + "Development", + "Meeting", + "Administration", + "Own Work", + "Studies", + "Testing", + ]; + + return ( + <> +
+ { + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + + + + + + + + + {activities.map((activity, index) => ( + + + + + ))} + +
Activity + Total Time (min) +
{activity} + { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
+
+ + ); +} + +export default NewTimeReport; diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index b86076a..42fb094 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,11 +1,11 @@ import { Link } from "react-router-dom"; -import { PublicUser } from "../Types/goTypes"; +import { User } from "../Types/Users"; /** * The props for the UserProps component */ interface UserProps { - users: PublicUser[]; + users: User[]; } /** @@ -23,9 +23,9 @@ export function UserListAdmin(props: UserProps): JSX.Element {
    {props.users.map((user) => ( - -
  • - {user.username} + +
  • + {user.userName}
  • ))} diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx deleted file mode 100644 index 423e793..0000000 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import React, { useEffect, useState } from "react"; -import { api } from "../API/API"; -import { Project } from "../Types/goTypes"; - -const UserProjectListAdmin: React.FC = () => { - const [projects, setProjects] = useState([]); - - useEffect(() => { - const fetchProjects = async (): Promise => { - try { - const token = localStorage.getItem("accessToken") ?? ""; - const username = getUsernameFromContext(); // Assuming you have a function to get the username from your context - - const response = await api.getUserProjects(username, 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 fetchProjects(); - }, []); - - return ( -
    -

    User Projects

    -
      - {projects.map((project) => ( -
    • - {project.name} - {/* Add any additional project details you want to display */} -
    • - ))} -
    -
    - ); -}; - -export default UserProjectListAdmin; diff --git a/frontend/src/Pages/AdminPages/AdminAddProject.tsx b/frontend/src/Pages/AdminPages/AdminAddProject.tsx index aedbd3f..9fd8bed 100644 --- a/frontend/src/Pages/AdminPages/AdminAddProject.tsx +++ b/frontend/src/Pages/AdminPages/AdminAddProject.tsx @@ -1,16 +1,26 @@ -import AddProject from "../../Components/AddProject"; -import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; +import Button from "../../Components/Button"; function AdminAddProject(): JSX.Element { - const content = ; + const content = <>; const buttons = ( <> - +
diff --git a/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx b/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx index 9f233a1..8f7bf9b 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx @@ -12,12 +12,11 @@ function ChangeRole(): JSX.Element { onClick={(): void => { return; }} - type="button" /> ); - return ; + return ; } export default ChangeRole; diff --git a/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx index c76947b..19ebbd9 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMOtherUsersTR.tsx @@ -10,6 +10,6 @@ function PMOtherUsersTR(): JSX.Element { ); - return ; + return ; } export default PMOtherUsersTR; diff --git a/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx b/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx index 9fe96cf..2af1145 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMProjectMembers.tsx @@ -14,7 +14,6 @@ function PMProjectMembers(): JSX.Element { onClick={(): void => { return; }} - type={"button"} /> @@ -23,13 +22,12 @@ function PMProjectMembers(): JSX.Element { onClick={(): void => { return; }} - type={"button"} /> ); - return ; + return ; } export default PMProjectMembers; diff --git a/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx b/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx index bd4e6ef..8e724f3 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx @@ -31,6 +31,6 @@ function PMProjectPage(): JSX.Element { ); - return ; + return ; } export default PMProjectPage; diff --git a/frontend/src/Pages/ProjectManagerPages/PMTotalTimeActivity.tsx b/frontend/src/Pages/ProjectManagerPages/PMTotalTimeActivity.tsx index 676ea28..dd84fd7 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMTotalTimeActivity.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMTotalTimeActivity.tsx @@ -1,6 +1,6 @@ -import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; -import TimeReport from "../../Components/NewWeeklyReport"; +import TimeReport from "../../Components/TimeReport"; +import BackButton from "../../Components/BackButton"; function PMTotalTimeActivity(): JSX.Element { const content = ( @@ -18,6 +18,6 @@ function PMTotalTimeActivity(): JSX.Element { ); - return ; + return ; } export default PMTotalTimeActivity; diff --git a/frontend/src/Pages/ProjectManagerPages/PMTotalTimeRole.tsx b/frontend/src/Pages/ProjectManagerPages/PMTotalTimeRole.tsx index c0161f8..05d902f 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMTotalTimeRole.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMTotalTimeRole.tsx @@ -10,6 +10,6 @@ function PMTotalTimeRole(): JSX.Element { ); - return ; + return ; } export default PMTotalTimeRole; diff --git a/frontend/src/Pages/ProjectManagerPages/PMUnsignedReports.tsx b/frontend/src/Pages/ProjectManagerPages/PMUnsignedReports.tsx index 713efec..fae3842 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMUnsignedReports.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMUnsignedReports.tsx @@ -10,6 +10,6 @@ function PMUnsignedReports(): JSX.Element { ); - return ; + return ; } export default PMUnsignedReports; diff --git a/frontend/src/Pages/ProjectManagerPages/PMViewUnsignedReport.tsx b/frontend/src/Pages/ProjectManagerPages/PMViewUnsignedReport.tsx index aea25fb..3bbf3ee 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMViewUnsignedReport.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMViewUnsignedReport.tsx @@ -1,7 +1,7 @@ -import BackButton from "../../Components/BackButton"; import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; -import TimeReport from "../../Components/NewWeeklyReport"; +import TimeReport from "../../Components/TimeReport"; +import BackButton from "../../Components/BackButton"; function PMViewUnsignedReport(): JSX.Element { const content = ( @@ -20,19 +20,17 @@ function PMViewUnsignedReport(): JSX.Element { onClick={(): void => { return; }} - type="button" />