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 b5e1981..c13308b 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -21,13 +21,14 @@ 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) 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) } @@ -51,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 = ?" @@ -88,23 +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 } -func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP - _, err := d.Exec(addTimeReport, userName, projectName, 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 +} + +// 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) @@ -122,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 7650739..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) } @@ -343,3 +343,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 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/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index fea0dfd..91d46a9 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" @@ -225,3 +226,24 @@ 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) +} 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/Register.tsx b/frontend/src/Components/Register.tsx index e4a3ba0..0c0fcd0 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -4,6 +4,32 @@ import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; +function InputField(props: { + label: string; + type: string; + value: string; + onChange: (e: React.ChangeEvent) => void; +}): JSX.Element { + return ( +
+ + +
+ ); +} + export default function Register(): JSX.Element { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -14,7 +40,7 @@ export default function Register(): JSX.Element { }; return ( -
+
Register New User -
- - { - setUsername(e.target.value); - }} - /> -
-
- - { - setPassword(e.target.value); - }} - /> -
+ { + setUsername(e.target.value); + }} + /> + { + setPassword(e.target.value); + }} + />
+
); } - -export default NewTimeReport; diff --git a/frontend/src/Pages/AdminPages/AdminAddUser.tsx b/frontend/src/Pages/AdminPages/AdminAddUser.tsx index 5e8c01f..c0f9492 100644 --- a/frontend/src/Pages/AdminPages/AdminAddUser.tsx +++ b/frontend/src/Pages/AdminPages/AdminAddUser.tsx @@ -1,18 +1,16 @@ import BasicWindow from "../../Components/BasicWindow"; import Button from "../../Components/Button"; +import Register from "../../Components/Register"; function AdminAddUser(): JSX.Element { - const content = <>; + const content = ( + <> + + + ); const buttons = ( <> -