diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 4efba3f..ef365cd 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -4,7 +4,6 @@ import ( "embed" "os" "path/filepath" - "time" "ttime/internal/types" "github.com/jmoiron/sqlx" @@ -15,19 +14,21 @@ 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) - AddTimeReport(projectName string, userName string, activityType string, start time.Time, end time.Time) 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 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) } @@ -49,27 +50,16 @@ 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 addTimeReport = `WITH UserLookup AS (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 time_reports (project_id, user_id, activity_type, start, end) - VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?);` + 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 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 { @@ -88,23 +78,42 @@ 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 } -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) +// 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) 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) @@ -122,23 +131,28 @@ 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 3b9f339..5438d66 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -2,7 +2,6 @@ package database import ( "testing" - "time" ) // Tests are not guaranteed to be sequential @@ -93,7 +92,7 @@ func TestPromoteToAdmin(t *testing.T) { } } -func TestAddTimeReport(t *testing.T) { +func TestAddWeeklyReport(t *testing.T) { db, err := setupState() if err != nil { t.Error("setupState failed:", err) @@ -109,12 +108,9 @@ func TestAddTimeReport(t *testing.T) { t.Error("AddProject failed:", err) } - var now = time.Now() - var then = now.Add(time.Hour) - - err = db.AddTimeReport("testproject", "testuser", "activity", now, then) + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) if err != nil { - t.Error("AddTimeReport failed:", err) + t.Error("AddWeeklyReport failed:", err) } } @@ -134,12 +130,9 @@ func TestAddUserToProject(t *testing.T) { t.Error("AddProject failed:", err) } - var now = time.Now() - var then = now.Add(time.Hour) - - err = db.AddTimeReport("testproject", "testuser", "activity", now, then) + err = db.AddWeeklyReport("testproject", "testuser", 1, 1, 1, 1, 1, 1, 1) if err != nil { - t.Error("AddTimeReport failed:", err) + t.Error("AddWeeklyReport failed:", err) } err = db.AddUserToProject("testuser", "testproject", "user") @@ -343,3 +336,38 @@ 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 deleted file mode 100644 index 7c169c2..0000000 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ /dev/null @@ -1,22 +0,0 @@ -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 new file mode 100644 index 0000000..0e29b97 --- /dev/null +++ b/backend/internal/database/migrations/0035_weekly_report.sql @@ -0,0 +1,14 @@ +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 deleted file mode 100644 index be406ff..0000000 --- a/backend/internal/database/migrations/0040_time_report_collections.sql +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index de9757d..0000000 --- a/backend/internal/database/migrations/0070_salts.sql +++ /dev/null @@ -1,16 +0,0 @@ --- 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 deleted file mode 100644 index d984d58..0000000 --- a/backend/internal/database/migrations/0080_activity_types.sql +++ /dev/null @@ -1,10 +0,0 @@ -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 fea0dfd..648b4ed 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -1,6 +1,7 @@ package handlers import ( + "strconv" "time" "ttime/internal/database" "ttime/internal/types" @@ -17,6 +18,7 @@ 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 @@ -76,12 +78,17 @@ 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 { - u := new(types.User) - if err := c.BodyParser(u); err != nil { - return c.Status(400).SendString(err.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(u.Username); err != nil { + if err := gs.Db.RemoveUser(username); err != nil { return c.Status(500).SendString(err.Error()) } @@ -103,8 +110,7 @@ func (gs *GState) Login(c *fiber.Ctx) error { user := c.FormValue("user") pass := c.FormValue("pass") - // Throws Unauthorized error - if user != "user" || pass != "pass" { + if !gs.Db.CheckUser(user, pass) { return c.SendStatus(fiber.StatusUnauthorized) } @@ -158,9 +164,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) - p.Owner = claims["name"].(string) + owner := claims["name"].(string) - if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil { + if err := gs.Db.AddProject(p.Name, p.Description, owner); err != nil { return c.Status(500).SendString(err.Error()) } @@ -225,3 +231,42 @@ 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 new file mode 100644 index 0000000..23624db --- /dev/null +++ b/backend/internal/types/WeeklyReport.go @@ -0,0 +1,21 @@ +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 8fcfaf5..7e1747f 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -8,9 +8,8 @@ type Project struct { Owner string `json:"owner" db:"owner_user_id"` } -// As it arrives from the client +// As it arrives from the client, Owner is derived from the JWT token 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 233ec71..e9dff67 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -16,6 +16,7 @@ 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 4e0935c..9ba2556 100644 --- a/backend/main.go +++ b/backend/main.go @@ -68,9 +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", gs.UserDelete) // Perhaps just use POST to avoid headaches + server.Delete("/api/userdelete/:username", 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 cf6a887..38a1853 100644 --- a/frontend/src/Components/Button.tsx +++ b/frontend/src/Components/Button.tsx @@ -1,14 +1,17 @@ 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 d0e3da6..e4a3ba0 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -1,6 +1,8 @@ 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(""); @@ -12,25 +14,32 @@ export default function Register(): JSX.Element { }; return ( -
-
+
+
{ e.preventDefault(); void handleRegister(); }} > -

Register new user

+ TTIME Logo +

+ Register New User +

- + />

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