diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 9e63395..5221e4d 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -1,9 +1,7 @@ package database import ( - "database/sql" "embed" - "fmt" "os" "path/filepath" "time" @@ -16,19 +14,15 @@ import ( type Database interface { // Insert a new user into the database, password should be hashed before calling AddUser(username string, password string) error - 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 - // AddTimeReport(projectname string, start time.Time, end time.Time) error - // AddUserToProject(username string, projectname string) error - // ChangeUserRole(username string, projectname string, role string) error - // AddTimeReport(projectname string, start time.Time, end time.Time) error - // AddUserToProject(username string, projectname string) error - ChangeUserRole(username string, projectname string, role string) error GetProjectId(projectname string) (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 } // This struct is a wrapper type that holds the database connection @@ -40,11 +34,16 @@ type Db struct { //go:embed migrations var scripts embed.FS +// TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" -const addTimeReport = "INSERT INTO activity (report_id, activity_nbr, start_time, end_time, break, comment) VALUES (?, ?, ?, ?, ?, ?)" // WIP -const addUserToProject = "INSERT INTO project_member (project_id, user_id, role) VALUES (?, ?, ?)" // WIP +const addTimeReport = `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, 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 = ?" // DbConnect connects to the database func DbConnect(dbpath string) Database { @@ -63,24 +62,8 @@ func DbConnect(dbpath string) Database { return &Db{db} } -func (d *Db) ChangeUserRole(username string, projectname string, role string) error { - userID, err := d.GetUserId(username) - if err != nil { - return err - } - - projectID, err := d.GetProjectId(projectname) - if err != nil { - return err - } - - // Update user role in the project using the correct table name - _, err = d.Exec("INSERT OR REPLACE INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)", userID, projectID, role) - return err -} - -func (d *Db) AddTimeReport(projectname string, start time.Time, end time.Time, breakTime uint32) error { // WIP - _, err := d.Exec(addTimeReport, projectname, 0, start, end, breakTime, false) +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 } @@ -97,13 +80,26 @@ func (d *Db) AddUserToProject(username string, projectname string, role string) panic(err2) } - _, err3 := d.Exec(addUserToProject, projectid, userid, role) + _, err3 := d.Exec(addUserToProject, userid, projectid, role) return err3 } -// func (d *Db) ChangeUserRole(username string, projectname string, role string) error { +func (d *Db) ChangeUserRole(username string, projectname string, role string) error { + var userid int + userid, err := d.GetUserId(username) + if err != nil { + panic(err) + } -// } + var projectid int + projectid, err2 := d.GetProjectId(projectname) + if err2 != nil { + panic(err2) + } + + _, err3 := d.Exec(changeUserRole, role, userid, projectid) + return err3 +} // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { @@ -131,13 +127,7 @@ func (d *Db) GetUserId(username string) (int, error) { func (d *Db) GetProjectId(projectname string) (int, error) { var id int err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname) - if err != nil { - if err == sql.ErrNoRows { - return 0, fmt.Errorf("project '%s' not found", projectname) - } - return 0, err - } - return id, nil + return id, err } // Creates a new project in the database, associated with a user diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index 08eee32..b5fe49f 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,40 +93,84 @@ func TestPromoteToAdmin(t *testing.T) { } } -func TestDbChangeUserRole(t *testing.T) { - // Set up the initial state +func TestAddTimeReport(t *testing.T) { db, err := setupState() if err != nil { t.Error("setupState failed:", err) } - // Add a user - err = db.AddUser("test", "password") + err = db.AddUser("testuser", "password") if err != nil { t.Error("AddUser failed:", err) } - // Add a project - err = db.AddProject("test_project", "project description", "test") + err = db.AddProject("testproject", "description", "testuser") if err != nil { t.Error("AddProject failed:", err) } - // Change user role - err = db.ChangeUserRole("test", "test_project", "project_manager") + var now = time.Now() + var then = now.Add(time.Hour) + + err = db.AddTimeReport("testproject", "testuser", now, then) + if err != nil { + t.Error("AddTimeReport failed:", err) + } +} + +func TestAddUserToProject(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) + } + + var now = time.Now() + var then = now.Add(time.Hour) + + err = db.AddTimeReport("testproject", "testuser", now, then) + if err != nil { + t.Error("AddTimeReport failed:", err) + } + + err = db.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } +} + +func TestChangeUserRole(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.AddUserToProject("testuser", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.ChangeUserRole("testuser", "testproject", "admin") if err != nil { t.Error("ChangeUserRole failed:", err) } } - -// func TestAddTimeReport(t *testing.T) { - -// } - -// func TestAddUserToProject(t *testing.T) { - -// } - -// func TestChangeUserRole(t *testing.T) { - -// } diff --git a/backend/internal/database/migrations/0020_projects.sql b/backend/internal/database/migrations/0020_projects.sql index adfb818..58d8e97 100644 --- a/backend/internal/database/migrations/0020_projects.sql +++ b/backend/internal/database/migrations/0020_projects.sql @@ -1,11 +1,9 @@ CREATE TABLE IF NOT EXISTS projects ( id INTEGER PRIMARY KEY, - projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE, description TEXT NOT NULL, owner_user_id INTEGER NOT NULL, FOREIGN KEY (owner_user_id) REFERENCES users (id) ); -CREATE INDEX IF NOT EXISTS projects_projectId_index ON projects (projectId); CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (owner_user_id); \ No newline at end of file diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql index e8f3ec1..76812a1 100644 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -1,10 +1,11 @@ CREATE TABLE IF NOT EXISTS time_reports ( id INTEGER PRIMARY KEY, - reportId TEXT DEFAULT (HEX(RANDOMBLOB(6))) NOT NULL UNIQUE, 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 diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 24854c0..f33c87c 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -9,6 +9,8 @@ interface API { removeUser(username: string): Promise; /** Create a project */ createProject(project: NewProject): Promise; + /** Renew the token */ + renewToken(token: string): Promise; } // Export an instance of the API @@ -42,4 +44,14 @@ export const api: API = { body: JSON.stringify(project), }).then((res) => res.json() as Promise); }, + + 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/Register.tsx b/frontend/src/Components/Register.tsx new file mode 100644 index 0000000..8181774 --- /dev/null +++ b/frontend/src/Components/Register.tsx @@ -0,0 +1,74 @@ +import { useState } from "react"; +import { NewUser, User } from "../Types/Users"; +import { api } from "../API/API"; + +export default function Register(): JSX.Element { + const [username, setUsername] = useState(""); + const [password, setPassword] = useState(""); + + const handleRegister = async (): Promise => { + const newUser: NewUser = { userName: username, password }; + const user = await api.registerUser(newUser); + }; + + return ( +
+
+
{ + e.preventDefault(); + void handleRegister(); + }} + > +

Register new user

+
+ + { + setUsername(e.target.value); + }} + /> +
+
+ + { + setPassword(e.target.value); + }} + /> +
+
+ +
+
+

+
+
+ ); +} diff --git a/frontend/src/Pages/LoginPage.tsx b/frontend/src/Pages/LoginPage.tsx index d8ea651..11a7da2 100644 --- a/frontend/src/Pages/LoginPage.tsx +++ b/frontend/src/Pages/LoginPage.tsx @@ -3,6 +3,7 @@ import Logo from "/src/assets/TTIMElogo.png"; import "./LoginPage.css"; import { useEffect } from "react"; import { Link } from "react-router-dom"; +import Register from "../Components/Register"; const PreloadBackgroundAnimation = (): JSX.Element => { useEffect(() => { @@ -69,6 +70,14 @@ function LoginPage(): JSX.Element { }} /> + +