Merge imbs

This commit is contained in:
Imbus 2024-03-14 14:37:50 +01:00
commit be04ba148d
10 changed files with 204 additions and 64 deletions

View file

@ -1,9 +1,7 @@
package database package database
import ( import (
"database/sql"
"embed" "embed"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -16,19 +14,15 @@ import (
type Database interface { type Database interface {
// Insert a new user into the database, password should be hashed before calling // Insert a new user into the database, password should be hashed before calling
AddUser(username string, password string) error AddUser(username string, password string) error
RemoveUser(username string) error RemoveUser(username string) error
PromoteToAdmin(username string) error PromoteToAdmin(username string) error
GetUserId(username string) (int, error) GetUserId(username string) (int, error)
AddProject(name string, description string, username string) error AddProject(name string, description string, username string) error
Migrate(dirname 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) 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 // This struct is a wrapper type that holds the database connection
@ -40,11 +34,16 @@ type Db struct {
//go:embed migrations //go:embed migrations
var scripts embed.FS 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 userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" 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 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 addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?),
const addUserToProject = "INSERT INTO project_member (project_id, user_id, role) VALUES (?, ?, ?)" // WIP 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 // DbConnect connects to the database
func DbConnect(dbpath string) Database { func DbConnect(dbpath string) Database {
@ -63,24 +62,8 @@ func DbConnect(dbpath string) Database {
return &Db{db} return &Db{db}
} }
func (d *Db) ChangeUserRole(username string, projectname string, role string) error { func (d *Db) AddTimeReport(projectName string, userName string, start time.Time, end time.Time) error { // WIP
userID, err := d.GetUserId(username) _, err := d.Exec(addTimeReport, userName, projectName, start, end)
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)
return err return err
} }
@ -97,13 +80,26 @@ func (d *Db) AddUserToProject(username string, projectname string, role string)
panic(err2) panic(err2)
} }
_, err3 := d.Exec(addUserToProject, projectid, userid, role) _, err3 := d.Exec(addUserToProject, userid, projectid, role)
return err3 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 // AddUser adds a user to the database
func (d *Db) AddUser(username string, password string) error { 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) { func (d *Db) GetProjectId(projectname string) (int, error) {
var id int var id int
err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname) err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname)
if err != nil { return id, err
if err == sql.ErrNoRows {
return 0, fmt.Errorf("project '%s' not found", projectname)
}
return 0, err
}
return id, nil
} }
// Creates a new project in the database, associated with a user // Creates a new project in the database, associated with a user

View file

@ -2,6 +2,7 @@ package database
import ( import (
"testing" "testing"
"time"
) )
// Tests are not guaranteed to be sequential // Tests are not guaranteed to be sequential
@ -92,40 +93,84 @@ func TestPromoteToAdmin(t *testing.T) {
} }
} }
func TestDbChangeUserRole(t *testing.T) { func TestAddTimeReport(t *testing.T) {
// Set up the initial state
db, err := setupState() db, err := setupState()
if err != nil { if err != nil {
t.Error("setupState failed:", err) t.Error("setupState failed:", err)
} }
// Add a user err = db.AddUser("testuser", "password")
err = db.AddUser("test", "password")
if err != nil { if err != nil {
t.Error("AddUser failed:", err) t.Error("AddUser failed:", err)
} }
// Add a project err = db.AddProject("testproject", "description", "testuser")
err = db.AddProject("test_project", "project description", "test")
if err != nil { if err != nil {
t.Error("AddProject failed:", err) t.Error("AddProject failed:", err)
} }
// Change user role var now = time.Now()
err = db.ChangeUserRole("test", "test_project", "project_manager") 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 { if err != nil {
t.Error("ChangeUserRole failed:", err) t.Error("ChangeUserRole failed:", err)
} }
} }
// func TestAddTimeReport(t *testing.T) {
// }
// func TestAddUserToProject(t *testing.T) {
// }
// func TestChangeUserRole(t *testing.T) {
// }

View file

@ -1,11 +1,9 @@
CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
projectId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL UNIQUE, name VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL, description TEXT NOT NULL,
owner_user_id INTEGER NOT NULL, owner_user_id INTEGER NOT NULL,
FOREIGN KEY (owner_user_id) REFERENCES users (id) 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); CREATE INDEX IF NOT EXISTS projects_user_id_index ON projects (owner_user_id);

View file

@ -1,10 +1,11 @@
CREATE TABLE IF NOT EXISTS time_reports ( CREATE TABLE IF NOT EXISTS time_reports (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
reportId TEXT DEFAULT (HEX(RANDOMBLOB(6))) NOT NULL UNIQUE,
project_id INTEGER NOT NULL, project_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
start DATETIME NOT NULL, start DATETIME NOT NULL,
end DATETIME NOT NULL, end DATETIME NOT NULL,
FOREIGN KEY (project_id) REFERENCES projects (id) ON DELETE CASCADE 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 CREATE TRIGGER IF NOT EXISTS time_reports_start_before_end

View file

@ -9,6 +9,8 @@ interface API {
removeUser(username: string): Promise<User>; removeUser(username: string): Promise<User>;
/** Create a project */ /** Create a project */
createProject(project: NewProject): Promise<Project>; createProject(project: NewProject): Promise<Project>;
/** Renew the token */
renewToken(token: string): Promise<string>;
} }
// Export an instance of the API // Export an instance of the API
@ -42,4 +44,14 @@ export const api: API = {
body: JSON.stringify(project), body: JSON.stringify(project),
}).then((res) => res.json() as Promise<Project>); }).then((res) => res.json() as Promise<Project>);
}, },
async renewToken(token: string): Promise<string> {
return fetch("/api/loginrenew", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + token,
},
}).then((res) => res.json() as Promise<string>);
},
}; };

View file

@ -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<void> => {
const newUser: NewUser = { userName: username, password };
const user = await api.registerUser(newUser);
};
return (
<div>
<div className="w-full max-w-xs">
<form
className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4"
onSubmit={(e) => {
e.preventDefault();
void handleRegister();
}}
>
<h3 className="pb-2">Register new user</h3>
<div className="mb-4">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="username"
>
Username
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
id="username"
type="text"
placeholder="Username"
value={username}
onChange={(e) => {
setUsername(e.target.value);
}}
/>
</div>
<div className="mb-6">
<label
className="block text-gray-700 text-sm font-bold mb-2"
htmlFor="password"
>
Password
</label>
<input
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
id="password"
type="password"
placeholder="Choose your password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
</div>
<div className="flex items-center justify-between">
<button
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
type="submit"
>
Register
</button>
</div>
</form>
<p className="text-center text-gray-500 text-xs"></p>
</div>
</div>
);
}

View file

@ -3,6 +3,7 @@ import Logo from "/src/assets/TTIMElogo.png";
import "./LoginPage.css"; import "./LoginPage.css";
import { useEffect } from "react"; import { useEffect } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import Register from "../Components/Register";
const PreloadBackgroundAnimation = (): JSX.Element => { const PreloadBackgroundAnimation = (): JSX.Element => {
useEffect(() => { useEffect(() => {
@ -69,6 +70,14 @@ function LoginPage(): JSX.Element {
}} }}
/> />
</Link> </Link>
<Link to="/register">
<Button
text="Register new user"
onClick={(): void => {
return;
}}
/>
</Link>
</div> </div>
</div> </div>
</> </>

View file

@ -1,11 +1,11 @@
// This is how the API responds // This is how the API responds
export interface User { export interface User {
id: number; id: number;
name: string; userName: string;
} }
// Used to create a new user // Used to create a new user
export interface NewUser { export interface NewUser {
name: string; userName: string;
password: string; password: string;
} }

View file

@ -5,6 +5,7 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import LoginPage from "./Pages/LoginPage.tsx"; import LoginPage from "./Pages/LoginPage.tsx";
import YourProjectsPage from "./Pages/YourProjectsPage.tsx"; import YourProjectsPage from "./Pages/YourProjectsPage.tsx";
import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx"; import UserProjectPage from "./Pages/UserPages/UserProjectPage.tsx";
import Register from "./Components/Register.tsx";
// This is where the routes are mounted // This is where the routes are mounted
const router = createBrowserRouter([ const router = createBrowserRouter([
@ -20,6 +21,10 @@ const router = createBrowserRouter([
path: "/project", path: "/project",
element: <UserProjectPage />, element: <UserProjectPage />,
}, },
{
path: "/register",
element: <Register />,
},
]); ]);
// Semi-hacky way to get the root element // Semi-hacky way to get the root element

6
package-lock.json generated Normal file
View file

@ -0,0 +1,6 @@
{
"name": "TTime",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}