diff --git a/backend/Makefile b/backend/Makefile index 9cfa335..da0e254 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -118,3 +118,7 @@ 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 a0cf5b9..c13308b 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -21,7 +21,7 @@ type Database interface { AddProject(name string, description string, username string) error Migrate(dirname string) error GetProjectId(projectname string) (int, error) - AddTimeReport(projectName string, userName string, start time.Time, end time.Time) 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) @@ -52,8 +52,8 @@ const projectInsert = "INSERT INTO projects (name, description, owner_user_id) S 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 = ?), 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), ?, ?);` + 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 = ?" @@ -89,29 +89,34 @@ func DbConnect(dbpath string) Database { return &Db{db} } +// 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) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP - _, err := d.Exec(addTimeReport, userName, projectName, start, end) +// AddTimeReport adds a time report for a specific project and user. +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) @@ -129,23 +134,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 117c08a..9118e2f 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -112,7 +112,7 @@ func TestAddTimeReport(t *testing.T) { var now = time.Now() var then = now.Add(time.Hour) - err = db.AddTimeReport("testproject", "testuser", now, then) + err = db.AddTimeReport("testproject", "testuser", "activity", now, then) if err != nil { t.Error("AddTimeReport failed:", err) } @@ -137,7 +137,7 @@ func TestAddUserToProject(t *testing.T) { var now = time.Now() var then = now.Add(time.Hour) - err = db.AddTimeReport("testproject", "testuser", now, then) + err = db.AddTimeReport("testproject", "testuser", "activity", now, then) if err != nil { t.Error("AddTimeReport failed:", err) } diff --git a/backend/internal/database/migrations/0030_time_reports.sql b/backend/internal/database/migrations/0030_time_reports.sql index 76812a1..7c169c2 100644 --- a/backend/internal/database/migrations/0030_time_reports.sql +++ b/backend/internal/database/migrations/0030_time_reports.sql @@ -2,10 +2,12 @@ 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 diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql index b84dfac..de9757d 100644 --- a/backend/internal/database/migrations/0070_salts.sql +++ b/backend/internal/database/migrations/0070_salts.sql @@ -1,7 +1,7 @@ -- It is unclear weather this table will be used -- Create the table to store hash salts -CREATE TABLE salts ( +CREATE TABLE IF NOT EXISTS salts ( id INTEGER PRIMARY KEY, salt TEXT NOT NULL ); 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/tygo.yaml b/backend/tygo.yaml new file mode 100644 index 0000000..54c1e8f --- /dev/null +++ b/backend/tygo.yaml @@ -0,0 +1,9 @@ +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 447a464..1051c03 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'], + ignorePatterns: ['dist', '.eslintrc.cjs', 'tailwind.config.js', 'postcss.config.js', 'jest.config.cjs', 'goTypes.ts'], parser: '@typescript-eslint/parser', plugins: ['react-refresh', 'prettier'], rules: { diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index f33c87c..248ad37 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,57 +1,120 @@ import { NewProject, Project } from "../Types/Project"; import { NewUser, User } from "../Types/Users"; +// This type of pattern should be hard to misuse +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): Promise; + removeUser(username: string, token: string): Promise>; /** Create a project */ - createProject(project: NewProject): Promise; + createProject( + project: NewProject, + token: string, + ): Promise>; /** Renew the token */ - renewToken(token: string): Promise; + renewToken(token: string): Promise>; } // Export an instance of the API export const api: API = { - 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 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" }; + } else { + const data = (await response.json()) as User; + return { success: true, data }; + } + } catch (e) { + return { success: false, message: "Failed to register 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 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 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 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 renewToken(token: string): Promise { - return fetch("/api/loginrenew", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }).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" }; + } }, }; 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" />