diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index ef365cd..4efba3f 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -4,6 +4,7 @@ import ( "embed" "os" "path/filepath" + "time" "ttime/internal/types" "github.com/jmoiron/sqlx" @@ -14,21 +15,19 @@ 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(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, activityType 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) } @@ -50,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, activity_type, 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 { @@ -78,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, activityType string, start time.Time, end time.Time) error { // WIP + _, err := d.Exec(addTimeReport, userName, projectName, activityType, 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) @@ -131,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) diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 5438d66..3b9f339 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -2,6 +2,7 @@ package database import ( "testing" + "time" ) // Tests are not guaranteed to be sequential @@ -92,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) @@ -108,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", "activity", now, then) if err != nil { - t.Error("AddWeeklyReport failed:", err) + t.Error("AddTimeReport failed:", err) } } @@ -130,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", "activity", now, then) if err != nil { - t.Error("AddWeeklyReport failed:", err) + t.Error("AddTimeReport failed:", err) } err = db.AddUserToProject("testuser", "testproject", "user") @@ -336,38 +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") - } -} 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..7c169c2 --- /dev/null +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -0,0 +1,22 @@ +CREATE TABLE IF NOT EXISTS time_reports ( + id INTEGER PRIMARY KEY, + project_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + activity_type TEXT 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 + FOREIGN KEY (activity_type) REFERENCES activity_types (name) 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 0e29b97..0000000 --- a/backend/internal/database/migrations/0035_weekly_report.sql +++ /dev/null @@ -1,14 +0,0 @@ -CREATE TABLE weekly_reports ( - user_id INTEGER, - project_id INTEGER, - week INTEGER, - development_time INTEGER, - meeting_time INTEGER, - admin_time INTEGER, - own_work_time INTEGER, - study_time INTEGER, - testing_time INTEGER, - FOREIGN KEY (user_id) REFERENCES users(id), - FOREIGN KEY (project_id) REFERENCES projects(id) - PRIMARY KEY (user_id, project_id, week) -) \ 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..de9757d --- /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 IF NOT EXISTS 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/database/migrations/0080_activity_types.sql b/backend/internal/database/migrations/0080_activity_types.sql new file mode 100644 index 0000000..d984d58 --- /dev/null +++ b/backend/internal/database/migrations/0080_activity_types.sql @@ -0,0 +1,10 @@ +CREATE TABLE IF NOT EXISTS activity_types ( + name TEXT PRIMARY KEY +); + +INSERT OR IGNORE INTO activity_types (name) VALUES ('Development'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Meeting'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Administration'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Own Work'); +INSERT OR IGNORE INTO activity_types (name) VALUES ('Studies'); +INSErt OR IGNORE INTO activity_types (name) VALUES ('Testing'); \ No newline at end of file diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 648b4ed..fea0dfd 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,7 +1,6 @@ package handlers import ( - "strconv" "time" "ttime/internal/database" "ttime/internal/types" @@ -18,7 +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 // 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 @@ -78,17 +76,12 @@ func (gs *GState) Register(c *fiber.Ctx) error { // 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") + u := new(types.User) + if err := c.BodyParser(u); err != nil { + return c.Status(400).SendString(err.Error()) } - if err := gs.Db.RemoveUser(username); err != nil { + if err := gs.Db.RemoveUser(u.Username); err != nil { return c.Status(500).SendString(err.Error()) } @@ -110,7 +103,8 @@ func (gs *GState) Login(c *fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") - if !gs.Db.CheckUser(user, pass) { + // Throws Unauthorized error + if user != "user" || pass != "pass" { return c.SendStatus(fiber.StatusUnauthorized) } @@ -164,9 +158,9 @@ func (gs *GState) CreateProject(c *fiber.Ctx) 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) + p.Owner = claims["name"].(string) - if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { + if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil { return c.Status(500).SendString(err.Error()) } @@ -231,42 +225,3 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) 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) 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()) - } - - 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") -} diff --git a/backend/internal/types/WeeklyReport.go b/backend/internal/types/WeeklyReport.go deleted file mode 100644 index 23624db..0000000 --- a/backend/internal/types/WeeklyReport.go +++ /dev/null @@ -1,21 +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 - // The week number - Week int - // Total time spent on development - DevelopmentTime int - // Total time spent in meetings - MeetingTime int - // Total time spent on administrative tasks - AdminTime int - // Total time spent on personal projects - OwnWorkTime int - // Total time spent on studying - StudyTime int - // Total time spent on testing - TestingTime int -} 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 9ba2556..4e0935c 100644 --- a/backend/main.go +++ b/backend/main.go @@ -68,10 +68,9 @@ 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) // Announce the port we are listening on and start the server 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/Register.tsx b/frontend/src/Components/Register.tsx index e4a3ba0..d0e3da6 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -1,8 +1,6 @@ import { useState } from "react"; import { NewUser } from "../Types/Users"; import { api } from "../API/API"; -import Logo from "../assets/Logo.svg"; -import Button from "./Button"; export default function Register(): JSX.Element { const [username, setUsername] = useState(""); @@ -14,32 +12,25 @@ export default function Register(): JSX.Element { }; return ( -
-
+
+
{ e.preventDefault(); void handleRegister(); }} > - TTIME Logo -

- Register New User -

+

Register new user

-

diff --git a/frontend/src/Pages/AdminPages/AdminAddProject.tsx b/frontend/src/Pages/AdminPages/AdminAddProject.tsx index 2922400..9fd8bed 100644 --- a/frontend/src/Pages/AdminPages/AdminAddProject.tsx +++ b/frontend/src/Pages/AdminPages/AdminAddProject.tsx @@ -11,14 +11,12 @@ function AdminAddProject(): JSX.Element { onClick={(): void => { return; }} - type="button" />
diff --git a/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx b/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx index bd800a7..6d87b90 100644 --- a/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx +++ b/frontend/src/Pages/ProjectManagerPages/PMChangeRole.tsx @@ -11,14 +11,12 @@ function ChangeRole(): JSX.Element { onClick={(): void => { return; }} - type="button" />