diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 6bb113b..5a88873 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -4,6 +4,7 @@ import ( "embed" "os" "path/filepath" + "time" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" @@ -11,7 +12,9 @@ import ( // Interface for the database 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) @@ -20,6 +23,9 @@ type Database interface { // 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 } // This struct is a wrapper type that holds the database connection @@ -34,9 +40,8 @@ var scripts embed.FS 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 = "" -// const addUserToProject = "" +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 changeUserRole = "" // DbConnect connects to the database @@ -56,13 +61,27 @@ func DbConnect(dbpath string) Database { return &Db{db} } -// func (d *Db) AddTimeReport(projectname string, start time.Time, end time.Time) error { +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 +} -// } +func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP + var userid int + userid, err := d.GetUserId(username) + if err != nil { + panic(err) + } -// func (d *Db) AddUserToProject(username string, projectname string) error { + var projectid int + projectid, err2 := d.GetProjectId(projectname) + if err2 != nil { + panic(err2) + } -// } + _, err3 := d.Exec(addUserToProject, projectid, userid, role) + return err3 +} // func (d *Db) ChangeUserRole(username string, projectname string, role string) error { @@ -87,7 +106,13 @@ func (d *Db) PromoteToAdmin(username string) error { func (d *Db) GetUserId(username string) (int, error) { var id int - err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) + err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) // Borde det inte vara "user" i singular + return id, err +} + +func (d *Db) GetProjectId(projectname string) (int, error) { // WIP, denna kan vara goof + var id int + err := d.Get(&id, "SELECT id FROM project WHERE project_name = ?", projectname) return id, err } diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index 7cb23fd..5c9d329 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -1,3 +1,7 @@ +-- Id is a surrogate key for in ternal use +-- userId is what is used for external id +-- username is what is used for login +-- password is the hashed password CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, @@ -5,5 +9,6 @@ CREATE TABLE IF NOT EXISTS users ( password VARCHAR(255) NOT NULL ); +-- Users are commonly searched by username and userId CREATE INDEX IF NOT EXISTS users_username_index ON users (username); CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId); \ No newline at end of file diff --git a/backend/internal/database/migrations/0070_salts.sql b/backend/internal/database/migrations/0070_salts.sql new file mode 100644 index 0000000..b84dfac --- /dev/null +++ b/backend/internal/database/migrations/0070_salts.sql @@ -0,0 +1,16 @@ +-- It is unclear weather this table will be used + +-- Create the table to store hash salts +CREATE TABLE salts ( + id INTEGER PRIMARY KEY, + salt TEXT NOT NULL +); + +-- Commented out for now, no time for good practices, which is atrocious +-- Create a trigger to automatically generate a salt when inserting a new user record +-- CREATE TRIGGER generate_salt_trigger +-- AFTER INSERT ON users +-- BEGIN +-- INSERT INTO salts (salt) VALUES (randomblob(16)); +-- UPDATE users SET salt_id = (SELECT last_insert_rowid()) WHERE id = new.id; +-- END; diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 689759b..9c42133 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -11,11 +11,11 @@ import ( // The actual interface that we will use type GlobalState interface { - Register(c *fiber.Ctx) error // To register a new user - UserDelete(c *fiber.Ctx) error // To delete a user - Login(c *fiber.Ctx) error // To get the token - LoginRenew(c *fiber.Ctx) error // To renew the token - // CreateProject(c *fiber.Ctx) error // To create a new project + Register(c *fiber.Ctx) error // To register a new user + UserDelete(c *fiber.Ctx) error // To delete a user + Login(c *fiber.Ctx) error // To get the token + LoginRenew(c *fiber.Ctx) error // To renew the token + CreateProject(c *fiber.Ctx) error // To create a new project // GetProjects(c *fiber.Ctx) error // To get all projects // GetProject(c *fiber.Ctx) error // To get a specific project // UpdateProject(c *fiber.Ctx) error // To update a project @@ -58,7 +58,7 @@ type GState struct { // @Failure 500 {string} string "Internal server error" // @Router /api/register [post] func (gs *GState) Register(c *fiber.Ctx) error { - u := new(types.User) + u := new(types.NewUser) if err := c.BodyParser(u); err != nil { return c.Status(400).SendString(err.Error()) } @@ -142,3 +142,24 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error { } return c.JSON(fiber.Map{"token": t}) } + +// CreateProject is a simple handler that creates a new project +func (gs *GState) CreateProject(c *fiber.Ctx) error { + user := c.Locals("user").(*jwt.Token) + + p := new(types.NewProject) + if err := c.BodyParser(p); err != nil { + return c.Status(400).SendString(err.Error()) + } + + // Get the username from the token and set it as the owner of the project + // This is ugly but + claims := user.Claims.(jwt.MapClaims) + p.Owner = claims["name"].(string) + + if err := gs.Db.AddProject(p.Name, p.Description, p.Owner); err != nil { + return c.Status(500).SendString(err.Error()) + } + + return c.Status(200).SendString("Project added") +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go new file mode 100644 index 0000000..cabf6c6 --- /dev/null +++ b/backend/internal/types/project.go @@ -0,0 +1,21 @@ +package types + +import ( + "time" +) + +// Project is a struct that holds the information about a project +type Project struct { + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Owner string `json:"owner" db:"owner"` + Created time.Time `json:"created" db:"created"` +} + +// As it arrives from the client +type NewProject struct { + Name string `json:"name"` + Description string `json:"description"` + Owner string `json:"owner"` +} diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index fa735d7..233ec71 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -16,6 +16,11 @@ func (u *User) ToPublicUser() (*PublicUser, error) { }, nil } +type NewUser struct { + Username string `json:"username"` + Password string `json:"password"` +} + // PublicUser represents a user that is safe to send over the API (no password) type PublicUser struct { UserId string `json:"userId"` diff --git a/frontend/index.html b/frontend/index.html index e4b78ea..573ba58 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Vite + React + TS + TTIME
diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js index 2e7af2b..2aa7205 100644 --- a/frontend/postcss.config.js +++ b/frontend/postcss.config.js @@ -3,4 +3,4 @@ export default { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx new file mode 100644 index 0000000..1835d6a --- /dev/null +++ b/frontend/src/Components/BasicWindow.tsx @@ -0,0 +1,22 @@ +import Header from "./Header"; +import Footer from "./Footer"; + +function BasicWindow({ + username, + content, + buttons, +}: { + username: string; + content: React.ReactNode; + buttons: React.ReactNode; +}): JSX.Element { + return ( +
+
+
{content}
+ +
+ ); +} + +export default BasicWindow; diff --git a/frontend/src/Components/Button.tsx b/frontend/src/Components/Button.tsx new file mode 100644 index 0000000..cf6a887 --- /dev/null +++ b/frontend/src/Components/Button.tsx @@ -0,0 +1,18 @@ +function Button({ + text, + onClick, +}: { + text: string; + onClick: () => void; +}): JSX.Element { + return ( + + ); +} + +export default Button; diff --git a/frontend/src/Components/Footer.tsx b/frontend/src/Components/Footer.tsx new file mode 100644 index 0000000..a3b7469 --- /dev/null +++ b/frontend/src/Components/Footer.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +function Footer({ children }: { children: React.ReactNode }): JSX.Element { + return ( + + ); +} + +export default Footer; diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx new file mode 100644 index 0000000..5c642b8 --- /dev/null +++ b/frontend/src/Components/Header.tsx @@ -0,0 +1,54 @@ +import { useState } from "react"; +import { Link } from "react-router-dom"; + +function Header({ username }: { username: string }): JSX.Element { + const [isOpen, setIsOpen] = useState(false); + + const handleLogout = (): void => { + // Add any logout logic here + }; + + return ( +
+ + TTIME Logo + + +
{ + setIsOpen(true); + }} + onMouseLeave={() => { + setIsOpen(false); + }} + > + + + {isOpen && ( +
+ + + +
+ )} +
+
+ ); +} + +export default Header; diff --git a/frontend/src/Pages/LoginPage.css b/frontend/src/Pages/LoginPage.css new file mode 100644 index 0000000..0b2f5db --- /dev/null +++ b/frontend/src/Pages/LoginPage.css @@ -0,0 +1,26 @@ +body{ + overflow: hidden; +} + +@keyframes backgroundTransition { + 0% { + background-image: url('src/assets/1.jpg'); + animation-timing-function: ease-out; + } + 25% { + background-image: url('src/assets/2.jpg'); + animation-timing-function: ease-in; + } + 50% { + background-image: url('src/assets/3.jpg'); + animation-timing-function: ease-out; + } + 75% { + background-image: url('src/assets/4.jpg'); + animation-timing-function: ease-in; + } + 100% { + background-image: url('src/assets/1.jpg'); + animation-timing-function: ease-out; + } +} \ No newline at end of file diff --git a/frontend/src/Pages/LoginPage.tsx b/frontend/src/Pages/LoginPage.tsx new file mode 100644 index 0000000..d8ea651 --- /dev/null +++ b/frontend/src/Pages/LoginPage.tsx @@ -0,0 +1,78 @@ +import Button from "../Components/Button"; +import Logo from "/src/assets/TTIMElogo.png"; +import "./LoginPage.css"; +import { useEffect } from "react"; +import { Link } from "react-router-dom"; + +const PreloadBackgroundAnimation = (): JSX.Element => { + useEffect(() => { + const images = [ + "src/assets/1.jpg", + "src/assets/2.jpg", + "src/assets/3.jpg", + "src/assets/4.jpg", + ]; + + // Pre-load images + for (const i of images) { + console.log(i); + } + + // Start animation + document.body.style.animation = "backgroundTransition 30s infinite"; + }, []); + + return <>; +}; + +function LoginPage(): JSX.Element { + return ( + <> + +
+
+ TTIME Logo +

+ {" "} + Welcome to TTIME!{" "} +

+

+ {" "} + Please log in to continue{" "} +

+ + + +
+
+ + ); +} + +export default LoginPage; diff --git a/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx b/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx new file mode 100644 index 0000000..009a5bd --- /dev/null +++ b/frontend/src/Pages/ProjectManagerPages/PMProjectPage.tsx @@ -0,0 +1,38 @@ +import BasicWindow from "../../Components/BasicWindow"; +import Button from "../../Components/Button"; + +function PMProjectPage(): JSX.Element { + const content = ( + <> +

ProjectNameExample

+
+

+ Your Time Reports +

+

+ New Time Report +

+

+ Statistics +

+

+ Unsigned Time Reports +

+
+ + ); + + const buttons = ( + <> +