diff --git a/backend/Makefile b/backend/Makefile index 65a2f3c..da0e254 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -10,7 +10,6 @@ DB_FILE = db.sqlite3 # Directory containing migration SQL scripts MIGRATIONS_DIR = internal/database/migrations -SAMPLE_DATA_DIR = internal/database/sample_data # Build target build: @@ -55,14 +54,6 @@ migrate: sqlite3 $(DB_FILE) < $$file; \ done -sampledata: - @echo "If this ever fails, run make clean and try again" - @echo "Migrating database $(DB_FILE) using SQL scripts in $(SAMPLE_DATA_DIR)" - @for file in $(wildcard $(SAMPLE_DATA_DIR)/*.sql); do \ - echo "Applying migration: $$file"; \ - sqlite3 $(DB_FILE) < $$file; \ - done - # Target added primarily for CI/CD to ensure that the database is created before running tests db.sqlite3: make migrate diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 322c812..75908b4 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -19,167 +19,23 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/login": { - "post": { - "description": "logs the user in and returns a jwt token", - "consumes": [ - "application/json" - ], - "produces": [ - "text/plain" - ], - "tags": [ - "User" - ], - "summary": "login", - "parameters": [ - { - "description": "login info", - "name": "NewUser", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/types.NewUser" - } - } - ], - "responses": { - "200": { - "description": "Successfully signed token for user", - "schema": { - "type": "Token" - } - }, - "400": { - "description": "Bad request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "string" - } - } - } - } - }, - "/loginerenew": { - "post": { - "security": [ - { - "bererToken": [] - } - ], - "description": "renews the users token", - "consumes": [ - "application/json" - ], - "produces": [ - "text/plain" - ], - "tags": [ - "User" - ], - "summary": "LoginRenews", - "responses": { - "200": { - "description": "Successfully signed token for user", - "schema": { - "type": "Token" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "string" - } - } - } - } - }, - "/promoteToAdmin": { - "post": { - "description": "promote chosen user to admin", - "consumes": [ - "application/json" - ], - "produces": [ - "text/plain" - ], - "tags": [ - "User" - ], - "summary": "PromoteToAdmin", - "parameters": [ - { - "description": "user info", - "name": "NewUser", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/types.NewUser" - } - } - ], - "responses": { - "200": { - "description": "Successfully prometed user", - "schema": { - "type": "json" - } - }, - "400": { - "description": "bad request", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "string" - } - } - } - } - }, - "/register": { + "/api/register": { "post": { "description": "Register a new user", "consumes": [ "application/json" ], "produces": [ - "text/plain" + "application/json" ], "tags": [ "User" ], - "summary": "Register", + "summary": "Register a new user", "parameters": [ { "description": "User to register", - "name": "NewUser", + "name": "{string}", "in": "body", "required": true, "schema": { @@ -208,82 +64,6 @@ const docTemplate = `{ } } } - }, - "/userdelete/{username}": { - "delete": { - "description": "UserDelete deletes a user from the database", - "consumes": [ - "application/json" - ], - "produces": [ - "text/plain" - ], - "tags": [ - "User" - ], - "summary": "UserDelete", - "responses": { - "200": { - "description": "User deleted", - "schema": { - "type": "string" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "string" - } - }, - "403": { - "description": "You can only delete yourself", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "string" - } - } - } - } - }, - "/users/all": { - "get": { - "description": "lists all users", - "consumes": [ - "application/json" - ], - "produces": [ - "text/plain" - ], - "tags": [ - "User" - ], - "summary": "ListsAllUsers", - "responses": { - "200": { - "description": "Successfully signed token for user", - "schema": { - "type": "json" - } - }, - "401": { - "description": "Unauthorized", - "schema": { - "type": "string" - } - }, - "500": { - "description": "Internal server error", - "schema": { - "type": "string" - } - } - } - } } }, "definitions": { @@ -299,13 +79,6 @@ const docTemplate = `{ } } }, - "securityDefinitions": { - "bererToken": { - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - }, "externalDocs": { "description": "OpenAPI", "url": "https://swagger.io/resources/open-api/" diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 25dd04b..e2aa366 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -20,7 +20,6 @@ type Database interface { GetUserId(username string) (int, error) AddProject(name string, description string, username string) error Migrate() error - MigrateSampleData() error GetProjectId(projectname string) (int, error) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error @@ -50,9 +49,6 @@ type UserProjectMember struct { //go:embed migrations var scripts embed.FS -//go:embed sample_data -var sampleData 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 = ?" @@ -64,10 +60,9 @@ const addWeeklyReport = `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 p.id, p.name, p.description FROM projects p - JOIN user_roles ur ON p.id = ur.project_id - JOIN users u ON ur.user_id = u.id - WHERE u.username = ?` +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 { @@ -383,42 +378,3 @@ func (d *Db) Migrate() error { return nil } - -// MigrateSampleData applies sample data to the database. -func (d *Db) MigrateSampleData() error { - // Insert sample data - files, err := sampleData.ReadDir("sample_data") - if err != nil { - return err - } - - if len(files) == 0 { - println("No sample data files found") - } - tr := d.MustBegin() - - // Iterate over each SQL file and execute it - for _, file := range files { - if file.IsDir() || filepath.Ext(file.Name()) != ".sql" { - continue - } - - // This is perhaps not the most elegant way to do this - sqlBytes, err := sampleData.ReadFile("sample_data/" + file.Name()) - if err != nil { - return err - } - - sqlQuery := string(sqlBytes) - _, err = tr.Exec(sqlQuery) - if err != nil { - return err - } - } - - if tr.Commit() != nil { - return err - } - - return nil -} diff --git a/backend/internal/database/migrations/0010_users.sql b/backend/internal/database/migrations/0010_users.sql index 15b1373..d2e2dd1 100644 --- a/backend/internal/database/migrations/0010_users.sql +++ b/backend/internal/database/migrations/0010_users.sql @@ -4,9 +4,11 @@ -- password is the hashed password CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, + userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE, 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/sample_data/0010_sample_data.sql b/backend/internal/database/sample_data/0010_sample_data.sql deleted file mode 100644 index 4dac91b..0000000 --- a/backend/internal/database/sample_data/0010_sample_data.sql +++ /dev/null @@ -1,35 +0,0 @@ -INSERT OR IGNORE INTO users(username, password) -VALUES ("admin", "123"); - -INSERT OR IGNORE INTO users(username, password) -VALUES ("user", "123"); - -INSERT OR IGNORE INTO users(username, password) -VALUES ("user2", "123"); - -INSERT OR IGNORE INTO projects(name,description,owner_user_id) -VALUES ("projecttest","test project", 1); - -INSERT OR IGNORE INTO projects(name,description,owner_user_id) -VALUES ("projecttest2","test project2", 1); - -INSERT OR IGNORE INTO projects(name,description,owner_user_id) -VALUES ("projecttest3","test project3", 1); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (1,1,"project_manager"); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (2,1,"member"); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (3,1,"member"); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (3,2,"member"); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (3,3,"member"); - -INSERT OR IGNORE INTO user_roles(user_id,project_id,p_role) -VALUES (2,1,"project_manager"); diff --git a/backend/internal/handlers/global_state.go b/backend/internal/handlers/global_state.go index 932451d..566d549 100644 --- a/backend/internal/handlers/global_state.go +++ b/backend/internal/handlers/global_state.go @@ -34,17 +34,29 @@ type GlobalState interface { // UpdateCollection(c *fiber.Ctx) error // To update a collection // DeleteCollection(c *fiber.Ctx) error // To delete a collection // SignCollection(c *fiber.Ctx) error // To sign a collection - ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database - ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project - ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project + GetButtonCount(c *fiber.Ctx) error // For demonstration purposes + IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes + ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database + ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project + ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project } // "Constructor" func NewGlobalState(db database.Database) GlobalState { - return &GState{Db: db} + return &GState{Db: db, ButtonCount: 0} } // The global state, which implements all the handlers type GState struct { - Db database.Database + Db database.Database + ButtonCount int +} + +func (gs *GState) GetButtonCount(c *fiber.Ctx) error { + return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) +} + +func (gs *GState) IncrementButtonCount(c *fiber.Ctx) error { + gs.ButtonCount++ + return c.Status(200).JSON(fiber.Map{"pressCount": gs.ButtonCount}) } diff --git a/backend/internal/handlers/handlers_project_related.go b/backend/internal/handlers/handlers_project_related.go index f3a7ea0..3732249 100644 --- a/backend/internal/handlers/handlers_project_related.go +++ b/backend/internal/handlers/handlers_project_related.go @@ -5,7 +5,6 @@ import ( "ttime/internal/types" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" "github.com/golang-jwt/jwt/v5" ) @@ -68,47 +67,37 @@ func (gs *GState) GetProject(c *fiber.Ctx) error { // Extract the project ID from the request parameters or body projectID := c.Params("projectID") if projectID == "" { - log.Info("No project ID provided") return c.Status(400).SendString("No project ID provided") } - log.Info("Getting project with ID: ", projectID) + println("Getting project with ID: ", projectID) // Parse the project ID into an integer projectIDInt, err := strconv.Atoi(projectID) if err != nil { - log.Info("Invalid project ID") 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 { - log.Info("Error getting project:", err) return c.Status(500).SendString(err.Error()) } // Return the project as JSON - log.Info("Returning project: ", project.Name) + println("Returning project: ", project.Name) return c.JSON(project) } func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { // Extract the project name from the request parameters or body projectName := c.Params("projectName") - if projectName == "" { - log.Info("No project name provided") - return c.Status(400).SendString("No project name provided") - } // Get all users associated with the project from the database users, err := gs.Db.GetAllUsersProject(projectName) if err != nil { - log.Info("Error getting users for project:", err) return c.Status(500).SendString(err.Error()) } - log.Info("Returning users for project: ", projectName) - // Return the list of users as JSON return c.JSON(users) } @@ -122,7 +111,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { Role string `json:"role"` } if err := c.BodyParser(&requestData); err != nil { - log.Info("Error parsing request body:", err) + println("Error parsing request body:", err) return c.Status(400).SendString("Bad request") } @@ -130,27 +119,27 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) adminUsername := claims["name"].(string) - log.Info("Admin username from claims:", adminUsername) + println("Admin username from claims:", adminUsername) isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) if err != nil { - log.Info("Error checking admin status:", err) + println("Error checking admin status:", err) return c.Status(500).SendString(err.Error()) } if !isAdmin { - log.Info("User is not a site admin:", adminUsername) + println("User is not a site admin:", adminUsername) return c.Status(403).SendString("User is not a site admin") } // Add the user to the project with the specified role err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) if err != nil { - log.Info("Error adding user to project:", err) + println("Error adding user to project:", err) return c.Status(500).SendString(err.Error()) } // Return success message - log.Info("User added to project successfully:", requestData.Username) + println("User added to project successfully:", requestData.Username) return c.SendStatus(fiber.StatusOK) } diff --git a/backend/internal/handlers/handlers_report_related.go b/backend/internal/handlers/handlers_report_related.go index 85eb6e2..291d068 100644 --- a/backend/internal/handlers/handlers_report_related.go +++ b/backend/internal/handlers/handlers_report_related.go @@ -5,7 +5,6 @@ import ( "ttime/internal/types" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" "github.com/golang-jwt/jwt/v5" ) @@ -17,62 +16,50 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error { report := new(types.NewWeeklyReport) if err := c.BodyParser(report); err != nil { - log.Info("Error parsing weekly report") return c.Status(400).SendString(err.Error()) } // Make sure all the fields of the report are valid if report.Week < 1 || report.Week > 52 { - log.Info("Invalid week number") return c.Status(400).SendString("Invalid week number") } if report.DevelopmentTime < 0 || report.MeetingTime < 0 || report.AdminTime < 0 || report.OwnWorkTime < 0 || report.StudyTime < 0 || report.TestingTime < 0 { - log.Info("Invalid time report") return c.Status(400).SendString("Invalid time report") } if err := gs.Db.AddWeeklyReport(report.ProjectName, username, report.Week, report.DevelopmentTime, report.MeetingTime, report.AdminTime, report.OwnWorkTime, report.StudyTime, report.TestingTime); err != nil { - log.Info("Error adding weekly report") return c.Status(500).SendString(err.Error()) } - log.Info("Weekly report added") return c.Status(200).SendString("Time report added") } // Handler for retrieving weekly report func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { // Extract the necessary parameters from the request + println("GetWeeklyReport") user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) username := claims["name"].(string) - log.Info("Getting weekly report for: ", username) - // Extract project name and week from query parameters projectName := c.Query("projectName") + println(projectName) week := c.Query("week") - - if projectName == "" || week == "" { - log.Info("Missing project name or week number") - return c.Status(400).SendString("Missing project name or week number") - } + println(week) // Convert week to integer weekInt, err := strconv.Atoi(week) if err != nil { - log.Info("Invalid week number") return c.Status(400).SendString("Invalid week number") } // Call the database function to get the weekly report report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) if err != nil { - log.Info("Error getting weekly report from db:", err) return c.Status(500).SendString(err.Error()) } - log.Info("Returning weekly report") // Return the retrieved weekly report return c.JSON(report) } @@ -82,33 +69,35 @@ type ReportId struct { } func (gs *GState) SignReport(c *fiber.Ctx) error { + println("Signing report...") // Extract the necessary parameters from the token user := c.Locals("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) projectManagerUsername := claims["name"].(string) - log.Info("Signing report for: ", projectManagerUsername) - // Extract report ID from the request query parameters // reportID := c.Query("reportId") rid := new(ReportId) if err := c.BodyParser(rid); err != nil { return err } - log.Info("Signing report for: ", rid.ReportId) + println("Signing report for: ", rid.ReportId) + // reportIDInt, err := strconv.Atoi(rid.ReportId) + // println("Signing report for: ", rid.ReportId) + // if err != nil { + // return c.Status(400).SendString("Invalid report ID") + // } // Get the project manager's ID projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) if err != nil { - log.Info("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID") } - log.Info("Project manager ID: ", projectManagerID) + println("blabla", projectManagerID) // Call the database function to sign the weekly report err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID) if err != nil { - log.Info("Error signing weekly report:", err) return c.Status(500).SendString(err.Error()) } diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 96fddb7..9c98d4d 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -1,57 +1,44 @@ package handlers import ( + "fmt" "time" "ttime/internal/types" - "github.com/gofiber/fiber/v2/log" - "github.com/gofiber/fiber/v2" "github.com/golang-jwt/jwt/v5" ) // Register is a simple handler that registers a new user // -// @Summary Register +// @Summary Register a new user // @Description Register a new user // @Tags User // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "User to register" -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" // @Router /register [post] func (gs *GState) Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") + println("Error parsing body") return c.Status(400).SendString(err.Error()) } - log.Info("Adding user:", u.Username) + println("Adding user:", u.Username) if err := gs.Db.AddUser(u.Username, u.Password); err != nil { - log.Warn("Error adding user:", err) return c.Status(500).SendString(err.Error()) } - log.Info("User added:", u.Username) + println("User added:", u.Username) return c.Status(200).SendString("User added") } // This path should obviously be protected in the future // UserDelete deletes a user from the database -// -// @Summary UserDelete -// @Description UserDelete deletes a user from the database -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {string} string "User deleted" -// @Failure 403 {string} string "You can only delete yourself" -// @Failure 500 {string} string "Internal server error" -// @Failure 401 {string} string "Unauthorized" -// @Router /userdelete/{username} [delete] func (gs *GState) UserDelete(c *fiber.Ctx) error { // Read from path parameters username := c.Params("username") @@ -60,44 +47,28 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error { auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) if username != auth_username { - log.Info("User tried to delete another user") return c.Status(403).SendString("You can only delete yourself") } if err := gs.Db.RemoveUser(username); err != nil { - log.Warn("Error deleting user:", err) return c.Status(500).SendString(err.Error()) } - log.Info("User deleted:", username) return c.Status(200).SendString("User deleted") } // Login is a simple login handler that returns a JWT token -// -// @Summary login -// @Description logs the user in and returns a jwt token -// @Tags User -// @Accept json -// @Param NewUser body types.NewUser true "login info" -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] func (gs *GState) Login(c *fiber.Ctx) error { // The body type is identical to a NewUser - u := new(types.NewUser) if err := c.BodyParser(u); err != nil { - log.Warn("Error parsing body") + println("Error parsing body") return c.Status(400).SendString(err.Error()) } - log.Info("Username logging in:", u.Username) + println("Username:", u.Username) if !gs.Db.CheckUser(u.Username, u.Password) { - log.Info("User not found") + println("User not found") return c.SendStatus(fiber.StatusUnauthorized) } @@ -110,36 +81,23 @@ func (gs *GState) Login(c *fiber.Ctx) error { // Create token token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - log.Info("Token created for user:", u.Username) + println("Token created for user:", u.Username) // Generate encoded token and send it as response. t, err := token.SignedString([]byte("secret")) if err != nil { - log.Warn("Error signing token") + println("Error signing token") return c.SendStatus(fiber.StatusInternalServerError) } println("Successfully signed token for user:", u.Username) - return c.JSON(types.Token{Token: t}) + return c.JSON(fiber.Map{"token": t}) } // LoginRenew is a simple handler that renews the token -// -// @Summary LoginRenews -// @Description renews the users token -// @Security bererToken -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 Token types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginerenew [post] func (gs *GState) LoginRenew(c *fiber.Ctx) error { + // For testing: curl localhost:3000/restricted -H "Authorization: Bearer " user := c.Locals("user").(*jwt.Token) - - log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - claims := user.Claims.(jwt.MapClaims) claims["exp"] = time.Now().Add(time.Hour * 72).Unix() renewed := jwt.MapClaims{ @@ -150,49 +108,23 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error { token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) t, err := token.SignedString([]byte("secret")) if err != nil { - log.Warn("Error signing token") return c.SendStatus(fiber.StatusInternalServerError) } - - log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) - return c.JSON(types.Token{Token: t}) + return c.JSON(fiber.Map{"token": t}) } // ListAllUsers is a handler that returns a list of all users in the application database -// -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Accept json -// @Produce plain -// @Success 200 {json} json "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] func (gs *GState) ListAllUsers(c *fiber.Ctx) error { // Get all users from the database users, err := gs.Db.GetAllUsersApplication() if err != nil { - log.Info("Error getting users from db:", err) // Debug print return c.Status(500).SendString(err.Error()) } - log.Info("Returning all users") // Return the list of users as JSON return c.JSON(users) } -// @Summary PromoteToAdmin -// @Description promote chosen user to admin -// @Tags User -// @Accept json -// @Produce plain -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {json} json "Successfully prometed user" -// @Failure 400 {string} string "bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser @@ -201,15 +133,15 @@ func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { } username := newUser.Username - log.Info("Promoting user to admin:", username) // Debug print + println("Promoting user to admin:", username) // Debug print // Promote the user to a site admin in the database if err := gs.Db.PromoteToAdmin(username); err != nil { - log.Info("Error promoting user to admin:", err) // Debug print + fmt.Println("Error promoting user to admin:", err) // Debug print return c.Status(500).SendString(err.Error()) } - log.Info("User promoted to admin successfully:", username) // Debug print + println("User promoted to admin successfully:", username) // Debug print // Return a success message return c.SendStatus(fiber.StatusOK) diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index d3f2170..e9dff67 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -27,8 +27,3 @@ type PublicUser struct { UserId string `json:"userId"` Username string `json:"username"` } - -// wrapper type for token -type Token struct { - Token string `json:"token"` -} diff --git a/backend/main.go b/backend/main.go index 76c5f99..3e2fb75 100644 --- a/backend/main.go +++ b/backend/main.go @@ -10,7 +10,6 @@ import ( "github.com/BurntSushi/toml" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/swagger" jwtware "github.com/gofiber/contrib/jwt" @@ -23,10 +22,6 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -//@securityDefinitions.apikey bererToken -//@in header -//@name Authorization - // @host localhost:8080 // @BasePath /api @@ -51,12 +46,6 @@ func main() { // Migrate the database if err = db.Migrate(); err != nil { fmt.Println("Error migrating database: ", err) - os.Exit(1) - } - - if err = db.MigrateSampleData(); err != nil { - fmt.Println("Error migrating sample data: ", err) - os.Exit(1) } // Get our global state @@ -64,9 +53,6 @@ func main() { // Create the server server := fiber.New() - server.Use(logger.New()) - - // Mounts the swagger documentation, this is available at /swagger/index.html server.Get("/swagger/*", swagger.HandlerDefault) // Mount our static files (Beware of the security implications of this!) @@ -75,6 +61,11 @@ func main() { // Register our unprotected routes server.Post("/api/register", gs.Register) + + // Register handlers for example button count + server.Get("/api/button", gs.GetButtonCount) + server.Post("/api/button", gs.IncrementButtonCount) + server.Post("/api/login", gs.Login) // Every route from here on will require a valid JWT @@ -82,7 +73,6 @@ func main() { SigningKey: jwtware.SigningKey{Key: []byte("secret")}, })) - // Protected routes (require a valid JWT bearer token authentication header) server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) @@ -93,7 +83,7 @@ func main() { server.Post("/api/signReport", gs.SignReport) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) - server.Get("/api/users/all", gs.ListAllUsers) + // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) if err != nil { diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index 6078513..e8566b6 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -46,7 +46,6 @@ interface API { username: string, token: string, ): Promise>; - /** Gets a project from id*/ getProject(id: number): Promise>; } @@ -150,10 +149,7 @@ export const api: API = { } }, - async getUserProjects( - username: string, - token: string, - ): Promise> { + async getUserProjects(token: string): Promise> { try { const response = await fetch("/api/getUserProjects", { method: "GET", @@ -161,7 +157,6 @@ export const api: API = { "Content-Type": "application/json", Authorization: "Bearer " + token, }, - body: JSON.stringify({ username }), }); if (!response.ok) { diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx deleted file mode 100644 index 45814e3..0000000 --- a/frontend/src/Components/AddProject.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { useState } from "react"; -import { APIResponse, api } from "../API/API"; -import { NewProject, Project } from "../Types/goTypes"; -import InputField from "./InputField"; -import Logo from "../assets/Logo.svg"; -import Button from "./Button"; - -/** - * Tries to add a project to the system - * @param props - Project name and description - * @returns {boolean} True if created, false if not - */ -function CreateProject(props: { name: string; description: string }): boolean { - const project: NewProject = { - name: props.name, - description: props.description, - }; - - let created = false; - - api - .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - created = true; - } else { - console.error(response.message); - } - }) - .catch((error) => { - console.error("An error occurred during creation:", error); - }); - return created; -} - -/** - * Tries to add a project to the system - * @returns {JSX.Element} UI for project adding - */ -function AddProject(): JSX.Element { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); - - return ( -
-
-
{ - e.preventDefault(); - CreateProject({ name: name, description: description }); - }} - > - TTIME Logo -

- Create a new project -

- { - setName(e.target.value); - }} - /> - { - setDescription(e.target.value); - }} - /> -
-
- -

-
-
- ); -} - -export default AddProject; diff --git a/frontend/src/Components/BasicWindow.tsx b/frontend/src/Components/BasicWindow.tsx index d5fd3b6..1835d6a 100644 --- a/frontend/src/Components/BasicWindow.tsx +++ b/frontend/src/Components/BasicWindow.tsx @@ -2,15 +2,17 @@ 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}
{buttons}
diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx deleted file mode 100644 index b0e8771..0000000 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ /dev/null @@ -1,247 +0,0 @@ -import { useState, useEffect } from "react"; -import { NewWeeklyReport } from "../Types/goTypes"; -import { api } from "../API/API"; -import { useNavigate } from "react-router-dom"; -import Button from "./Button"; - -export default function GetWeeklyReport(): JSX.Element { - const [projectName, setProjectName] = useState(""); - const [week, setWeek] = useState(0); - const [developmentTime, setDevelopmentTime] = useState(0); - const [meetingTime, setMeetingTime] = useState(0); - const [adminTime, setAdminTime] = useState(0); - const [ownWorkTime, setOwnWorkTime] = useState(0); - const [studyTime, setStudyTime] = useState(0); - const [testingTime, setTestingTime] = useState(0); - - const token = localStorage.getItem("accessToken") ?? ""; - const username = localStorage.getItem("username") ?? ""; - - useEffect(() => { - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - username, - projectName, - week.toString(), - token, - ); - - if (response.success) { - const report: NewWeeklyReport = response.data ?? { - projectName: "", - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setProjectName(report.projectName); - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; - - void fetchWeeklyReport(); - }, [projectName, token, username, week]); - - const handleNewWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { - projectName, - week, - developmentTime, - meetingTime, - adminTime, - ownWorkTime, - studyTime, - testingTime, - }; - - await api.submitWeeklyReport(newWeeklyReport, token); - }; - - const navigate = useNavigate(); - - return ( - <> -
-
{ - if (week === 0) { - alert("Please enter a week number"); - e.preventDefault(); - return; - } - e.preventDefault(); - void handleNewWeeklyReport(); - navigate("/project"); - }} - > -
- { - const weekNumber = parseInt(e.target.value.split("-W")[1]); - setWeek(weekNumber); - }} - onKeyDown={(event) => { - event.preventDefault(); - }} - onPaste={(event) => { - event.preventDefault(); - }} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Activity - - Total Time (min) -
Development - { - setDevelopmentTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Meeting - { - setMeetingTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Administration - { - setAdminTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Own Work - { - setOwnWorkTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Studies - { - setStudyTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
Testing - { - setTestingTime(parseInt(e.target.value)); - }} - onKeyDown={(event) => { - const keyValue = event.key; - if (!/\d/.test(keyValue) && keyValue !== "Backspace") - event.preventDefault(); - }} - /> -
-
-
-
- - ); -} diff --git a/frontend/src/Components/Header.tsx b/frontend/src/Components/Header.tsx index 819c5de..ba0a939 100644 --- a/frontend/src/Components/Header.tsx +++ b/frontend/src/Components/Header.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { Link } from "react-router-dom"; -function Header(): JSX.Element { +function Header({ username }: { username: string }): JSX.Element { const [isOpen, setIsOpen] = useState(false); const handleLogout = (): void => { - localStorage.clear(); + // Add any logout logic here }; return ( @@ -31,7 +31,7 @@ function Header(): JSX.Element { }} > {isOpen && ( diff --git a/frontend/src/Components/LoginCheck.tsx b/frontend/src/Components/LoginCheck.tsx index ce7d52c..3658cbf 100644 --- a/frontend/src/Components/LoginCheck.tsx +++ b/frontend/src/Components/LoginCheck.tsx @@ -10,22 +10,17 @@ function LoginCheck(props: { username: string; password: string; setAuthority: Dispatch>; -}): void { +}): number { const user: NewUser = { username: props.username, password: props.password, }; - - localStorage.clear(); - api .login(user) .then((response: APIResponse) => { if (response.success) { if (response.data !== undefined) { const token = response.data; - localStorage.setItem("accessToken", token); - localStorage.setItem("username", props.username); //TODO: change so that it checks for user type (admin, user, pm) instead if (token !== "" && props.username === "admin") { props.setAuthority((prevAuth) => { @@ -47,12 +42,14 @@ function LoginCheck(props: { console.error("Token was undefined"); } } else { - console.error("Token could not be fetched/No such user"); + console.error("Token could not be fetched"); } }) .catch((error) => { console.error("An error occurred during login:", error); }); + + return 0; } export default LoginCheck; diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 7b003cb..af77d36 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -3,9 +3,34 @@ import { NewUser } from "../Types/goTypes"; import { api } from "../API/API"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; -import InputField from "./InputField"; import { useNavigate } from "react-router-dom"; +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(); @@ -23,7 +48,6 @@ export default function Register(): JSX.Element { nav("/"); // Instantly navigate to the login page } else { setErrMessage(response.message ?? "Unknown error"); - console.error(errMessage); } }; @@ -48,7 +72,7 @@ export default function Register(): JSX.Element { { setUsername(e.target.value); }} @@ -56,11 +80,48 @@ export default function Register(): JSX.Element { { setPassword(e.target.value); }} /> +
+ + { + setUsername(e.target.value); + }} + /> +
+
+ + { + setPassword(e.target.value); + }} + /> +
+ {errMessage &&

{errMessage}

}