diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 5221e4d..e7f9251 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "time" + "ttime/internal/types" "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" @@ -23,6 +24,11 @@ type Database interface { 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 + GetAllUsersProject(projectname string) ([]UserProjectMember, error) + GetAllUsersApplication() ([]string, error) + GetProjectsForUser(username string) ([]types.Project, error) + GetAllProjects() ([]types.Project, error) + GetUserRole(username string, projectname string) (string, error) } // This struct is a wrapper type that holds the database connection @@ -31,6 +37,11 @@ type Db struct { *sqlx.DB } +type UserProjectMember struct { + Username string `db:"username"` + UserRole string `db:"p_role"` +} + //go:embed migrations var scripts embed.FS @@ -45,6 +56,21 @@ const addTimeReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = 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 = ?" +const getProjectsForUser = ` +SELECT + projects.id, + projects.name, + projects.description, + projects.owner_user_id +FROM + projects +JOIN + user_roles ON projects.id = user_roles.project_id +JOIN + users ON user_roles.user_id = users.id +WHERE + users.username = ?;` + // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database @@ -62,6 +88,18 @@ func DbConnect(dbpath string) Database { return &Db{db} } +func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { + var projects []types.Project + err := d.Select(&projects, getProjectsForUser, username) + return projects, err +} + +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) return err @@ -101,6 +139,12 @@ func (d *Db) ChangeUserRole(username string, projectname string, role string) er return err3 } +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) + return role, err +} + // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { _, err := d.Exec(userInsert, username, password) @@ -136,6 +180,69 @@ func (d *Db) AddProject(name string, description string, username string) error return err } +func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) { + // Define the SQL query to fetch users and their roles for a given project + query := ` + SELECT u.username, ur.p_role + FROM users u + INNER JOIN user_roles ur ON u.id = ur.user_id + INNER JOIN projects p ON ur.project_id = p.id + WHERE p.name = ? + ` + + // Execute the query + rows, err := d.Queryx(query, projectname) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var users []UserProjectMember + for rows.Next() { + var user UserProjectMember + if err := rows.StructScan(&user); err != nil { + return nil, err + } + users = append(users, user) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return users, nil +} + +// GetAllUsersApplication retrieves all usernames from the database +func (d *Db) GetAllUsersApplication() ([]string, error) { + // Define the SQL query to fetch all usernames + query := ` + SELECT username FROM users + ` + + // Execute the query + rows, err := d.Queryx(query) + if err != nil { + return nil, err + } + defer rows.Close() + + // Iterate over the rows and populate the result slice + var usernames []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, err + } + usernames = append(usernames, username) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return usernames, nil +} + // Reads a directory of migration files and applies them to the database. // This will eventually be used on an embedded directory func (d *Db) Migrate(dirname string) error { diff --git a/backend/internal/database/db_test.go b/backend/internal/database/db_test.go index b5fe49f..7650739 100644 --- a/backend/internal/database/db_test.go +++ b/backend/internal/database/db_test.go @@ -169,8 +169,177 @@ func TestChangeUserRole(t *testing.T) { t.Error("AddUserToProject failed:", err) } + role, err := db.GetUserRole("testuser", "testproject") + if err != nil { + t.Error("GetUserRole failed:", err) + } + if role != "user" { + t.Error("GetUserRole failed: expected user, got", role) + } + err = db.ChangeUserRole("testuser", "testproject", "admin") if err != nil { t.Error("ChangeUserRole failed:", err) } + + role, err = db.GetUserRole("testuser", "testproject") + if err != nil { + t.Error("GetUserRole failed:", err) + } + if role != "admin" { + t.Error("GetUserRole failed: expected admin, got", role) + } + +} + +func TestGetAllUsersProject(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser2", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddProject("testproject", "description", "testuser1") + if err != nil { + t.Error("AddProject failed:", err) + } + + err = db.AddUserToProject("testuser1", "testproject", "project_manager") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + err = db.AddUserToProject("testuser2", "testproject", "user") + if err != nil { + t.Error("AddUserToProject failed:", err) + } + + users, err := db.GetAllUsersProject("testproject") + if err != nil { + t.Error("GetAllUsersProject failed:", err) + } + + // Check if both users are returned with their roles + if len(users) != 2 { + t.Errorf("Expected 2 users, got %d", len(users)) + } + + // Check if testuser1 has project manager role + foundProjectManager := false + for _, user := range users { + if user.Username == "testuser1" && user.UserRole == "project_manager" { + foundProjectManager = true + break + } + } + if !foundProjectManager { + t.Error("Project Manager user not found") + } + + // Check if testuser2 has user role + foundUser := false + for _, user := range users { + if user.Username == "testuser2" && user.UserRole == "user" { + foundUser = true + break + } + } + if !foundUser { + t.Error("User user not found") + } +} + +func TestGetAllUsersApplication(t *testing.T) { + db, err := setupState() + if err != nil { + t.Error("setupState failed:", err) + } + + err = db.AddUser("testuser1", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + err = db.AddUser("testuser2", "password") + if err != nil { + t.Error("AddUser failed:", err) + } + + users, err := db.GetAllUsersApplication() + if err != nil { + t.Error("GetAllUsersApplication failed:", err) + } + + // Check if both users are returned + if len(users) != 2 { + t.Errorf("Expected 2 users, got %d", len(users)) + } + + // Check if the test users are included in the list + foundTestUser1 := false + foundTestUser2 := false + for _, user := range users { + if user == "testuser1" { + foundTestUser1 = true + } + if user == "testuser2" { + foundTestUser2 = true + } + } + + if !foundTestUser1 { + t.Error("testuser1 not found") + } + if !foundTestUser2 { + t.Error("testuser2 not found") + } +} + +func TestGetProjectsForUser(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) + } + + projects1, err := db.GetAllProjects() + if err != nil { + t.Error("GetAllProjects failed:", err) + } + + if len(projects1) != 1 { + t.Error("GetAllProjects failed: expected 1, got", len(projects1)) + } + + projects, err := db.GetProjectsForUser("testuser") + if err != nil { + t.Error("GetProjectsForUser failed:", err) + } + + if len(projects) != 1 { + t.Error("GetProjectsForUser failed: expected 1, got", len(projects)) + } } diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 9c42133..0f3fcc0 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -11,12 +11,12 @@ 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 - // GetProjects(c *fiber.Ctx) error // To get all projects + 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 + GetUserProjects(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 // DeleteProject(c *fiber.Ctx) error // To delete a project @@ -163,3 +163,20 @@ func (gs *GState) CreateProject(c *fiber.Ctx) error { return c.Status(200).SendString("Project added") } + +// GetUserProjects returns all projects that the user is a member of +func (gs *GState) GetUserProjects(c *fiber.Ctx) error { + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) + + // Then dip into the database to get the projects + projects, err := gs.Db.GetProjectsForUser(username) + if err != nil { + return c.Status(500).SendString(err.Error()) + } + + // Return a json serialized list of projects + return c.JSON(projects) +} diff --git a/backend/internal/types/project.go b/backend/internal/types/project.go index cabf6c6..8fcfaf5 100644 --- a/backend/internal/types/project.go +++ b/backend/internal/types/project.go @@ -1,16 +1,11 @@ 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"` + ID int `json:"id" db:"id"` + Name string `json:"name" db:"name"` + Description string `json:"description" db:"description"` + Owner string `json:"owner" db:"owner_user_id"` } // As it arrives from the client diff --git a/backend/main.go b/backend/main.go index bba3fa6..f7b0f7f 100644 --- a/backend/main.go +++ b/backend/main.go @@ -69,6 +69,7 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) + server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Post("/api/project", gs.CreateProject) diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index 5c642b8..ba0a939 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -15,7 +15,7 @@ function Header({ username }: { username: string }): JSX.Element { > TTIME Logo diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 8181774..d0e3da6 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { NewUser, User } from "../Types/Users"; +import { NewUser } from "../Types/Users"; import { api } from "../API/API"; export default function Register(): JSX.Element { @@ -8,7 +8,7 @@ export default function Register(): JSX.Element { const handleRegister = async (): Promise => { const newUser: NewUser = { userName: username, password }; - const user = await api.registerUser(newUser); + await api.registerUser(newUser); // TODO: Handle errors }; return ( diff --git a/frontend/src/Components/TimeReport.tsx b/frontend/src/Components/TimeReport.tsx new file mode 100644 index 0000000..c4ddc38 --- /dev/null +++ b/frontend/src/Components/TimeReport.tsx @@ -0,0 +1,59 @@ +function NewTimeReport(): JSX.Element { + const activities = [ + "Development", + "Meeting", + "Administration", + "Own Work", + "Studies", + "Testing", + ]; + + return ( + <> +
+ { + event.preventDefault(); + }} + onPaste={(event) => { + event.preventDefault(); + }} + /> + + + + + + + + + {activities.map((activity, index) => ( + + + + + ))} + +
Activity + Total Time (min) +
{activity} + { + const keyValue = event.key; + if (!/\d/.test(keyValue) && keyValue !== "Backspace") + event.preventDefault(); + }} + /> +
+
+ + ); +} + +export default NewTimeReport; diff --git a/frontend/src/Pages/AdminPages/AdminAddProject.tsx b/frontend/src/Pages/AdminPages/AdminAddProject.tsx new file mode 100644 index 0000000..9fd8bed --- /dev/null +++ b/frontend/src/Pages/AdminPages/AdminAddProject.tsx @@ -0,0 +1,26 @@ +import BasicWindow from "../../Components/BasicWindow"; +import Button from "../../Components/Button"; + +function AdminAddProject(): JSX.Element { + const content = <>; + + const buttons = ( + <> +