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
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

View file

@ -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) {
// }

View file

@ -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);

View file

@ -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

View file

@ -9,6 +9,8 @@ interface API {
removeUser(username: string): Promise<User>;
/** Create a project */
createProject(project: NewProject): Promise<Project>;
/** Renew the token */
renewToken(token: string): Promise<string>;
}
// Export an instance of the API
@ -42,4 +44,14 @@ export const api: API = {
body: JSON.stringify(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 { 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 {
}}
/>
</Link>
<Link to="/register">
<Button
text="Register new user"
onClick={(): void => {
return;
}}
/>
</Link>
</div>
</div>
</>

View file

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

View file

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