Merge branch 'frontend' into gruppPP

This commit is contained in:
Peter KW 2024-03-19 00:33:30 +01:00
commit b3dfbc47a4
23 changed files with 595 additions and 102 deletions

1
.gitignore vendored
View file

@ -36,6 +36,7 @@ dist/
.vscode/ .vscode/
.idea/ .idea/
.DS_Store .DS_Store
.go.work.sum
# Ignore configuration files # Ignore configuration files
.env .env

View file

@ -10,6 +10,7 @@ DB_FILE = db.sqlite3
# Directory containing migration SQL scripts # Directory containing migration SQL scripts
MIGRATIONS_DIR = internal/database/migrations MIGRATIONS_DIR = internal/database/migrations
SAMPLE_DATA_DIR = internal/database/sample_data
# Build target # Build target
build: build:
@ -54,6 +55,14 @@ migrate:
sqlite3 $(DB_FILE) < $$file; \ sqlite3 $(DB_FILE) < $$file; \
done 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 # Target added primarily for CI/CD to ensure that the database is created before running tests
db.sqlite3: db.sqlite3:
make migrate make migrate

View file

@ -20,6 +20,7 @@ type Database interface {
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() error Migrate() error
MigrateSampleData() error
GetProjectId(projectname string) (int, 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 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 AddUserToProject(username string, projectname string, role string) error
@ -32,6 +33,7 @@ type Database interface {
GetUserRole(username string, projectname string) (string, error) GetUserRole(username string, projectname string) (string, error)
GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error)
SignWeeklyReport(reportId int, projectManagerId int) error SignWeeklyReport(reportId int, projectManagerId int) error
IsSiteAdmin(username string) (bool, error)
} }
// This struct is a wrapper type that holds the database connection // This struct is a wrapper type that holds the database connection
@ -48,6 +50,9 @@ type UserProjectMember struct {
//go:embed migrations //go:embed migrations
var scripts embed.FS 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? // 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 = ?"
@ -59,9 +64,10 @@ 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 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 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 const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p
FROM projects JOIN user_roles ON projects.id = user_roles.project_id JOIN user_roles ur ON p.id = ur.project_id
JOIN users ON user_roles.user_id = users.id WHERE users.username = ?;` JOIN users u ON ur.user_id = u.id
WHERE u.username = ?`
// DbConnect connects to the database // DbConnect connects to the database
func DbConnect(dbpath string) Database { func DbConnect(dbpath string) Database {
@ -106,7 +112,10 @@ func (d *Db) GetAllProjects() ([]types.Project, error) {
// GetProject retrieves a specific project by its ID. // GetProject retrieves a specific project by its ID.
func (d *Db) GetProject(projectId int) (types.Project, error) { func (d *Db) GetProject(projectId int) (types.Project, error) {
var project types.Project var project types.Project
err := d.Select(&project, "SELECT * FROM projects WHERE id = ?") err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId)
if err != nil {
println("Error getting project: ", err)
}
return project, err return project, err
} }
@ -313,6 +322,26 @@ func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error {
return err return err
} }
// IsSiteAdmin checks if a given username is a site admin
func (d *Db) IsSiteAdmin(username string) (bool, error) {
// Define the SQL query to check if the user is a site admin
query := `
SELECT COUNT(*) FROM site_admin
JOIN users ON site_admin.admin_id = users.id
WHERE users.username = ?
`
// Execute the query
var count int
err := d.Get(&count, query, username)
if err != nil {
return false, err
}
// If count is greater than 0, the user is a site admin
return count > 0, nil
}
// Reads a directory of migration files and applies them to the database. // Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory // This will eventually be used on an embedded directory
func (d *Db) Migrate() error { func (d *Db) Migrate() error {
@ -354,3 +383,42 @@ func (d *Db) Migrate() error {
return nil 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
}

View file

@ -536,3 +536,33 @@ func TestSignWeeklyReportByAnotherProjectManager(t *testing.T) {
t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't") t.Error("Expected SignWeeklyReport to fail with a project manager who is not in the project, but it didn't")
} }
} }
func TestGetProject(t *testing.T) {
db, err := setupState()
if err != nil {
t.Error("setupState failed:", err)
}
// Add a user
err = db.AddUser("testuser", "password")
if err != nil {
t.Error("AddUser failed:", err)
}
// Add a project
err = db.AddProject("testproject", "description", "testuser")
if err != nil {
t.Error("AddProject failed:", err)
}
// Retrieve the added project
project, err := db.GetProject(1)
if err != nil {
t.Error("GetProject failed:", err)
}
// Check if the retrieved project matches the expected values
if project.Name != "testproject" {
t.Errorf("Expected Name to be testproject, got %s", project.Name)
}
}

View file

@ -4,11 +4,9 @@
-- password is the hashed password -- password is the hashed password
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
userId TEXT DEFAULT (HEX(RANDOMBLOB(4))) NOT NULL UNIQUE,
username VARCHAR(255) NOT NULL UNIQUE, username VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL password VARCHAR(255) NOT NULL
); );
-- Users are commonly searched by username and userId -- 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_username_index ON users (username);
CREATE INDEX IF NOT EXISTS users_userId_index ON users (userId);

View file

@ -0,0 +1,35 @@
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");

View file

@ -17,6 +17,9 @@ type GlobalState interface {
SubmitWeeklyReport(c *fiber.Ctx) error SubmitWeeklyReport(c *fiber.Ctx) error
GetWeeklyReport(c *fiber.Ctx) error GetWeeklyReport(c *fiber.Ctx) error
SignReport(c *fiber.Ctx) error SignReport(c *fiber.Ctx) error
GetProject(c *fiber.Ctx) error
AddUserToProjectHandler(c *fiber.Ctx) error
PromoteToAdmin(c *fiber.Ctx) error
// GetProject(c *fiber.Ctx) error // To get a specific project // GetProject(c *fiber.Ctx) error // To get a specific project
// UpdateProject(c *fiber.Ctx) error // To update a project // UpdateProject(c *fiber.Ctx) error // To update a project
// DeleteProject(c *fiber.Ctx) error // To delete a project // DeleteProject(c *fiber.Ctx) error // To delete a project
@ -31,8 +34,6 @@ type GlobalState interface {
// UpdateCollection(c *fiber.Ctx) error // To update a collection // UpdateCollection(c *fiber.Ctx) error // To update a collection
// DeleteCollection(c *fiber.Ctx) error // To delete a collection // DeleteCollection(c *fiber.Ctx) error // To delete a collection
// SignCollection(c *fiber.Ctx) error // To sign a collection // SignCollection(c *fiber.Ctx) error // To sign a collection
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 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 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 ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a project
@ -40,20 +41,10 @@ type GlobalState interface {
// "Constructor" // "Constructor"
func NewGlobalState(db database.Database) GlobalState { func NewGlobalState(db database.Database) GlobalState {
return &GState{Db: db, ButtonCount: 0} return &GState{Db: db}
} }
// The global state, which implements all the handlers // The global state, which implements all the handlers
type GState struct { 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})
} }

View file

@ -5,6 +5,7 @@ import (
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
@ -66,33 +67,90 @@ func (gs *GState) ProjectRoleChange(c *fiber.Ctx) error {
func (gs *GState) GetProject(c *fiber.Ctx) error { func (gs *GState) GetProject(c *fiber.Ctx) error {
// Extract the project ID from the request parameters or body // Extract the project ID from the request parameters or body
projectID := c.Params("projectID") 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)
// Parse the project ID into an integer // Parse the project ID into an integer
projectIDInt, err := strconv.Atoi(projectID) projectIDInt, err := strconv.Atoi(projectID)
if err != nil { if err != nil {
log.Info("Invalid project ID")
return c.Status(400).SendString("Invalid project ID") return c.Status(400).SendString("Invalid project ID")
} }
// Get the project from the database by its ID // Get the project from the database by its ID
project, err := gs.Db.GetProject(projectIDInt) project, err := gs.Db.GetProject(projectIDInt)
if err != nil { if err != nil {
log.Info("Error getting project:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
// Return the project as JSON // Return the project as JSON
log.Info("Returning project: ", project.Name)
return c.JSON(project) return c.JSON(project)
} }
func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error { func (gs *GState) ListAllUsersProject(c *fiber.Ctx) error {
// Extract the project name from the request parameters or body // Extract the project name from the request parameters or body
projectName := c.Params("projectName") 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 // Get all users associated with the project from the database
users, err := gs.Db.GetAllUsersProject(projectName) users, err := gs.Db.GetAllUsersProject(projectName)
if err != nil { if err != nil {
log.Info("Error getting users for project:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
log.Info("Returning users for project: ", projectName)
// Return the list of users as JSON // Return the list of users as JSON
return c.JSON(users) return c.JSON(users)
} }
// AddUserToProjectHandler is a handler that adds a user to a project with a specified role
func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
// Extract necessary parameters from the request
var requestData struct {
Username string `json:"username"`
ProjectName string `json:"projectName"`
Role string `json:"role"`
}
if err := c.BodyParser(&requestData); err != nil {
log.Info("Error parsing request body:", err)
return c.Status(400).SendString("Bad request")
}
// Check if the user adding another user to the project is a site admin
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
adminUsername := claims["name"].(string)
log.Info("Admin username from claims:", adminUsername)
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if err != nil {
log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error())
}
if !isAdmin {
log.Info("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)
return c.Status(500).SendString(err.Error())
}
// Return success message
log.Info("User added to project successfully:", requestData.Username)
return c.SendStatus(fiber.StatusOK)
}

View file

@ -5,6 +5,7 @@ import (
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/log"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
@ -16,78 +17,100 @@ func (gs *GState) SubmitWeeklyReport(c *fiber.Ctx) error {
report := new(types.NewWeeklyReport) report := new(types.NewWeeklyReport)
if err := c.BodyParser(report); err != nil { if err := c.BodyParser(report); err != nil {
log.Info("Error parsing weekly report")
return c.Status(400).SendString(err.Error()) return c.Status(400).SendString(err.Error())
} }
// Make sure all the fields of the report are valid // Make sure all the fields of the report are valid
if report.Week < 1 || report.Week > 52 { if report.Week < 1 || report.Week > 52 {
log.Info("Invalid week number")
return c.Status(400).SendString("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 { 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") 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 { 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()) return c.Status(500).SendString(err.Error())
} }
log.Info("Weekly report added")
return c.Status(200).SendString("Time report added") return c.Status(200).SendString("Time report added")
} }
// Handler for retrieving weekly report // Handler for retrieving weekly report
func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error { func (gs *GState) GetWeeklyReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the request // Extract the necessary parameters from the request
println("GetWeeklyReport")
user := c.Locals("user").(*jwt.Token) user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims) claims := user.Claims.(jwt.MapClaims)
username := claims["name"].(string) username := claims["name"].(string)
log.Info("Getting weekly report for: ", username)
// Extract project name and week from query parameters // Extract project name and week from query parameters
projectName := c.Query("projectName") projectName := c.Query("projectName")
println(projectName)
week := c.Query("week") week := c.Query("week")
println(week)
if projectName == "" || week == "" {
log.Info("Missing project name or week number")
return c.Status(400).SendString("Missing project name or week number")
}
// Convert week to integer // Convert week to integer
weekInt, err := strconv.Atoi(week) weekInt, err := strconv.Atoi(week)
if err != nil { if err != nil {
log.Info("Invalid week number")
return c.Status(400).SendString("Invalid week number") return c.Status(400).SendString("Invalid week number")
} }
// Call the database function to get the weekly report // Call the database function to get the weekly report
report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt) report, err := gs.Db.GetWeeklyReport(username, projectName, weekInt)
if err != nil { if err != nil {
log.Info("Error getting weekly report from db:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
log.Info("Returning weekly report")
// Return the retrieved weekly report // Return the retrieved weekly report
return c.JSON(report) return c.JSON(report)
} }
type ReportId struct {
ReportId int
}
func (gs *GState) SignReport(c *fiber.Ctx) error { func (gs *GState) SignReport(c *fiber.Ctx) error {
// Extract the necessary parameters from the token // Extract the necessary parameters from the token
user := c.Locals("user").(*jwt.Token) user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims) claims := user.Claims.(jwt.MapClaims)
managerUsername := claims["name"].(string) projectManagerUsername := claims["name"].(string)
// Extract the report ID and project manager ID from request parameters log.Info("Signing report for: ", projectManagerUsername)
reportID, err := strconv.Atoi(c.Params("reportId"))
if err != nil { // Extract report ID from the request query parameters
return c.Status(400).SendString("Invalid report ID") // reportID := c.Query("reportId")
rid := new(ReportId)
if err := c.BodyParser(rid); err != nil {
return err
} }
log.Info("Signing report for: ", rid.ReportId)
// Call the database function to get the project manager ID // Get the project manager's ID
managerID, err := gs.Db.GetUserId(managerUsername) projectManagerID, err := gs.Db.GetUserId(projectManagerUsername)
if err != nil { if err != nil {
log.Info("Failed to get project manager ID")
return c.Status(500).SendString("Failed to get project manager ID") return c.Status(500).SendString("Failed to get project manager ID")
} }
log.Info("Project manager ID: ", projectManagerID)
// Call the database function to sign the weekly report // Call the database function to sign the weekly report
err = gs.Db.SignWeeklyReport(reportID, managerID) err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID)
if err != nil { if err != nil {
return c.Status(500).SendString("Failed to sign the weekly report: " + err.Error()) log.Info("Error signing weekly report:", err)
return c.Status(500).SendString(err.Error())
} }
// Return success response
return c.Status(200).SendString("Weekly report signed successfully") return c.Status(200).SendString("Weekly report signed successfully")
} }

View file

@ -4,6 +4,8 @@ import (
"time" "time"
"ttime/internal/types" "ttime/internal/types"
"github.com/gofiber/fiber/v2/log"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
) )
@ -22,16 +24,17 @@ import (
func (gs *GState) Register(c *fiber.Ctx) error { func (gs *GState) Register(c *fiber.Ctx) error {
u := new(types.NewUser) u := new(types.NewUser)
if err := c.BodyParser(u); err != nil { if err := c.BodyParser(u); err != nil {
println("Error parsing body") log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error()) return c.Status(400).SendString(err.Error())
} }
println("Adding user:", u.Username) log.Info("Adding user:", u.Username)
if err := gs.Db.AddUser(u.Username, u.Password); err != nil { if err := gs.Db.AddUser(u.Username, u.Password); err != nil {
log.Warn("Error adding user:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
println("User added:", u.Username) log.Info("User added:", u.Username)
return c.Status(200).SendString("User added") return c.Status(200).SendString("User added")
} }
@ -44,14 +47,17 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read username from Locals // Read username from Locals
auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string) auth_username := c.Locals("user").(*jwt.Token).Claims.(jwt.MapClaims)["name"].(string)
if username == auth_username { if username != auth_username {
return c.Status(403).SendString("You can't delete yourself") 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 { if err := gs.Db.RemoveUser(username); err != nil {
log.Warn("Error deleting user:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
log.Info("User deleted:", username)
return c.Status(200).SendString("User deleted") return c.Status(200).SendString("User deleted")
} }
@ -60,13 +66,13 @@ func (gs *GState) Login(c *fiber.Ctx) error {
// The body type is identical to a NewUser // The body type is identical to a NewUser
u := new(types.NewUser) u := new(types.NewUser)
if err := c.BodyParser(u); err != nil { if err := c.BodyParser(u); err != nil {
println("Error parsing body") log.Warn("Error parsing body")
return c.Status(400).SendString(err.Error()) return c.Status(400).SendString(err.Error())
} }
println("Username:", u.Username) log.Info("Username logging in:", u.Username)
if !gs.Db.CheckUser(u.Username, u.Password) { if !gs.Db.CheckUser(u.Username, u.Password) {
println("User not found") log.Info("User not found")
return c.SendStatus(fiber.StatusUnauthorized) return c.SendStatus(fiber.StatusUnauthorized)
} }
@ -79,23 +85,25 @@ func (gs *GState) Login(c *fiber.Ctx) error {
// Create token // Create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
println("Token created for user:", u.Username) log.Info("Token created for user:", u.Username)
// Generate encoded token and send it as response. // Generate encoded token and send it as response.
t, err := token.SignedString([]byte("secret")) t, err := token.SignedString([]byte("secret"))
if err != nil { if err != nil {
println("Error signing token") log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError) return c.SendStatus(fiber.StatusInternalServerError)
} }
println("Successfully signed token for user:", u.Username) log.Info("Successfully signed token for user:", u.Username)
return c.JSON(fiber.Map{"token": t}) return c.JSON(fiber.Map{"token": t})
} }
// LoginRenew is a simple handler that renews the token // LoginRenew is a simple handler that renews the token
func (gs *GState) LoginRenew(c *fiber.Ctx) error { func (gs *GState) LoginRenew(c *fiber.Ctx) error {
// For testing: curl localhost:3000/restricted -H "Authorization: Bearer <token>"
user := c.Locals("user").(*jwt.Token) user := c.Locals("user").(*jwt.Token)
log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"])
claims := user.Claims.(jwt.MapClaims) claims := user.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(time.Hour * 72).Unix() claims["exp"] = time.Now().Add(time.Hour * 72).Unix()
renewed := jwt.MapClaims{ renewed := jwt.MapClaims{
@ -106,8 +114,11 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed)
t, err := token.SignedString([]byte("secret")) t, err := token.SignedString([]byte("secret"))
if err != nil { if err != nil {
log.Warn("Error signing token")
return c.SendStatus(fiber.StatusInternalServerError) return c.SendStatus(fiber.StatusInternalServerError)
} }
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])
return c.JSON(fiber.Map{"token": t}) return c.JSON(fiber.Map{"token": t})
} }
@ -116,9 +127,33 @@ func (gs *GState) ListAllUsers(c *fiber.Ctx) error {
// Get all users from the database // Get all users from the database
users, err := gs.Db.GetAllUsersApplication() users, err := gs.Db.GetAllUsersApplication()
if err != nil { if err != nil {
log.Info("Error getting users from db:", err) // Debug print
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
log.Info("Returning all users")
// Return the list of users as JSON // Return the list of users as JSON
return c.JSON(users) return c.JSON(users)
} }
func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body
var newUser types.NewUser
if err := c.BodyParser(&newUser); err != nil {
return c.Status(400).SendString("Bad request")
}
username := newUser.Username
log.Info("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
return c.Status(500).SendString(err.Error())
}
log.Info("User promoted to admin successfully:", username) // Debug print
// Return a success message
return c.SendStatus(fiber.StatusOK)
}

View file

@ -10,6 +10,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/swagger" "github.com/gofiber/swagger"
jwtware "github.com/gofiber/contrib/jwt" jwtware "github.com/gofiber/contrib/jwt"
@ -46,6 +47,12 @@ func main() {
// Migrate the database // Migrate the database
if err = db.Migrate(); err != nil { if err = db.Migrate(); err != nil {
fmt.Println("Error migrating database: ", err) 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 // Get our global state
@ -53,6 +60,9 @@ func main() {
// Create the server // Create the server
server := fiber.New() server := fiber.New()
server.Use(logger.New())
// Mounts the swagger documentation, this is available at /swagger/index.html
server.Get("/swagger/*", swagger.HandlerDefault) server.Get("/swagger/*", swagger.HandlerDefault)
// Mount our static files (Beware of the security implications of this!) // Mount our static files (Beware of the security implications of this!)
@ -61,11 +71,6 @@ func main() {
// Register our unprotected routes // Register our unprotected routes
server.Post("/api/register", gs.Register) 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) server.Post("/api/login", gs.Login)
// Every route from here on will require a valid JWT // Every route from here on will require a valid JWT
@ -73,12 +78,17 @@ func main() {
SigningKey: jwtware.SigningKey{Key: []byte("secret")}, SigningKey: jwtware.SigningKey{Key: []byte("secret")},
})) }))
// Protected routes (require a valid JWT bearer token authentication header)
server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Post("/api/submitReport", gs.SubmitWeeklyReport)
server.Get("/api/getUserProjects", gs.GetUserProjects) server.Get("/api/getUserProjects", gs.GetUserProjects)
server.Post("/api/loginrenew", gs.LoginRenew) server.Post("/api/loginrenew", gs.LoginRenew)
server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches
server.Post("/api/project", gs.CreateProject) server.Post("/api/project", gs.CreateProject)
server.Get("/api/project/:projectId", gs.GetProject)
server.Get("/api/getWeeklyReport", gs.GetWeeklyReport) server.Get("/api/getWeeklyReport", gs.GetWeeklyReport)
server.Post("/api/signReport", gs.SignReport)
server.Put("/api/addUserToProject", gs.AddUserToProjectHandler)
server.Post("/api/promoteToAdmin", gs.PromoteToAdmin)
// Announce the port we are listening on and start the server // Announce the port we are listening on and start the server
err = server.Listen(fmt.Sprintf(":%d", conf.Port)) err = server.Listen(fmt.Sprintf(":%d", conf.Port))

View file

@ -29,11 +29,6 @@ interface API {
project: NewProject, project: NewProject,
token: string, token: string,
): Promise<APIResponse<Project>>; ): Promise<APIResponse<Project>>;
/** Gets all the projects of a user*/
getUserProjects(
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Submit a weekly report */ /** Submit a weekly report */
submitWeeklyReport( submitWeeklyReport(
project: NewWeeklyReport, project: NewWeeklyReport,
@ -46,6 +41,10 @@ interface API {
week: string, week: string,
token: string, token: string,
): Promise<APIResponse<NewWeeklyReport>>; ): Promise<APIResponse<NewWeeklyReport>>;
/** Gets all the projects of a user*/
getUserProjects(token: string): Promise<APIResponse<Project[]>>;
/** Gets a project from id*/
getProject(id: number): Promise<APIResponse<Project>>;
} }
// Export an instance of the API // Export an instance of the API
@ -170,7 +169,7 @@ export const api: API = {
} catch (e) { } catch (e) {
return Promise.resolve({ return Promise.resolve({
success: false, success: false,
message: "Failed to get user projects", message: "API fucked",
}); });
} }
}, },
@ -253,4 +252,30 @@ export const api: API = {
return Promise.resolve({ success: false, message: "Failed to login" }); return Promise.resolve({ success: false, message: "Failed to login" });
} }
}, },
// Gets a projet by id, currently untested since we have no javascript-based tests
async getProject(id: number): Promise<APIResponse<Project>> {
try {
const response = await fetch(`/api/project/${id}`, {
method: "GET",
});
if (!response.ok) {
return {
success: false,
message: "Failed to get project: Response code " + response.status,
};
} else {
const data = (await response.json()) as Project;
return { success: true, data };
}
// The code below is garbage but satisfies the linter
// This needs fixing, do not copy this pattern
} catch (e: unknown) {
return {
success: false,
message: "Failed to get project: " + (e as Error).toString(),
};
}
},
}; };

View file

@ -50,8 +50,8 @@ export default function GetWeeklyReport(): JSX.Element {
} }
}; };
fetchWeeklyReport(); void fetchWeeklyReport();
}, []); }, [projectName, token, username, week]);
const handleNewWeeklyReport = async (): Promise<void> => { const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = { const newWeeklyReport: NewWeeklyReport = {

View file

@ -1,5 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import backgroundImage from "../assets/1.jpg";
function Header(): JSX.Element { function Header(): JSX.Element {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -11,7 +12,7 @@ function Header(): JSX.Element {
return ( return (
<header <header
className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover" className="fixed top-0 left-0 right-0 border-[1.75px] border-black text-black p-3 pl-5 flex items-center justify-between bg-cover"
style={{ backgroundImage: `url('src/assets/1.jpg')` }} style={{ backgroundImage: `url(${backgroundImage})` }}
> >
<Link to="/your-projects"> <Link to="/your-projects">
<img <img

View file

@ -1,9 +1,8 @@
import { useState, useContext } from "react"; import { useState } from "react";
import { NewWeeklyReport } from "../Types/goTypes"; import { NewWeeklyReport } from "../Types/goTypes";
import { api } from "../API/API"; import { api } from "../API/API";
import { useNavigate } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import Button from "./Button"; import Button from "./Button";
import { ProjectNameContext } from "../Pages/YourProjectsPage";
export default function NewWeeklyReport(): JSX.Element { export default function NewWeeklyReport(): JSX.Element {
const [week, setWeek] = useState(0); const [week, setWeek] = useState(0);
@ -14,12 +13,12 @@ export default function NewWeeklyReport(): JSX.Element {
const [studyTime, setStudyTime] = useState(0); const [studyTime, setStudyTime] = useState(0);
const [testingTime, setTestingTime] = useState(0); const [testingTime, setTestingTime] = useState(0);
const projectName = useContext(ProjectNameContext); const { projectName } = useParams();
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const handleNewWeeklyReport = async (): Promise<void> => { const handleNewWeeklyReport = async (): Promise<void> => {
const newWeeklyReport: NewWeeklyReport = { const newWeeklyReport: NewWeeklyReport = {
projectName, projectName: projectName ?? "",
week, week,
developmentTime, developmentTime,
meetingTime, meetingTime,
@ -46,7 +45,7 @@ export default function NewWeeklyReport(): JSX.Element {
} }
e.preventDefault(); e.preventDefault();
void handleNewWeeklyReport(); void handleNewWeeklyReport();
navigate("/project"); navigate(-1);
}} }}
> >
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">

View file

@ -48,7 +48,7 @@ export default function Register(): JSX.Element {
<InputField <InputField
label="Username" label="Username"
type="text" type="text"
value={username} value={username ?? ""}
onChange={(e) => { onChange={(e) => {
setUsername(e.target.value); setUsername(e.target.value);
}} }}
@ -56,7 +56,7 @@ export default function Register(): JSX.Element {
<InputField <InputField
label="Password" label="Password"
type="password" type="password"
value={password} value={password ?? ""}
onChange={(e) => { onChange={(e) => {
setPassword(e.target.value); setPassword(e.target.value);
}} }}

View file

@ -13,7 +13,7 @@ function App(): JSX.Element {
} else if (authority === 2) { } else if (authority === 2) {
navigate("/pm"); navigate("/pm");
} else if (authority === 3) { } else if (authority === 3) {
navigate("/user"); navigate("/yourProjects");
} }
}, [authority, navigate]); }, [authority, navigate]);

View file

@ -0,0 +1,18 @@
import Button from "../Components/Button";
export default function NotFoundPage(): JSX.Element {
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-white">
<h1 className="text-[30px]">404 Page Not Found</h1>
<a href="/">
<Button
text="Go to Home Page"
onClick={(): void => {
localStorage.clear();
}}
type="button"
/>
</a>
</div>
);
}

View file

@ -1,18 +1,20 @@
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation, useParams } from "react-router-dom";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import BackButton from "../../Components/BackButton"; import BackButton from "../../Components/BackButton";
function UserProjectPage(): JSX.Element { function UserProjectPage(): JSX.Element {
const { projectName } = useParams();
const content = ( const content = (
<> <>
<h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1> <h1 className="font-bold text-[30px] mb-[20px]">{useLocation().state}</h1>
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
<Link to="/project-page"> <Link to={`/projectPage/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
Your Time Reports Your Time Reports
</h1> </h1>
</Link> </Link>
<Link to="/new-time-report"> <Link to={`/newTimeReport/${projectName}`}>
<h1 className="font-bold underline text-[30px] cursor-pointer"> <h1 className="font-bold underline text-[30px] cursor-pointer">
New Time Report New Time Report
</h1> </h1>

View file

@ -1,4 +1,4 @@
import React, { useState, createContext, useEffect } from "react"; import { useState, createContext, useEffect } from "react";
import { Project } from "../Types/goTypes"; import { Project } from "../Types/goTypes";
import { api } from "../API/API"; import { api } from "../API/API";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
@ -7,10 +7,10 @@ import BasicWindow from "../Components/BasicWindow";
export const ProjectNameContext = createContext(""); export const ProjectNameContext = createContext("");
function UserProjectPage(): JSX.Element { function UserProjectPage(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]); /* const [projects, setProjects] = useState<Project[]>([]);
const [selectedProject, setSelectedProject] = useState(""); */ const [selectedProject, setSelectedProject] = useState("");
const getProjects = async (): Promise<void> => { /* const getProjects = async (): Promise<void> => {
const username = localStorage.getItem("username") ?? ""; // replace with actual username const username = localStorage.getItem("username") ?? ""; // replace with actual username
const token = localStorage.getItem("accessToken") ?? ""; // replace with actual token const token = localStorage.getItem("accessToken") ?? ""; // replace with actual token
const response = await api.getUserProjects(username, token); const response = await api.getUserProjects(username, token);
@ -24,7 +24,15 @@ function UserProjectPage(): JSX.Element {
// Call getProjects when the component mounts // Call getProjects when the component mounts
useEffect(() => { useEffect(() => {
getProjects(); getProjects();
}, []); }, []); */
// Mock data
const projects: Project[] = [
{ id: "1", name: "Project Test App" },
{ id: "2", name: "Project 2" },
{ id: "3", name: "Project 3" },
// Add more mock projects as needed
];
const handleProjectClick = (projectName: string): void => { const handleProjectClick = (projectName: string): void => {
setSelectedProject(projectName); setSelectedProject(projectName);
@ -36,7 +44,7 @@ function UserProjectPage(): JSX.Element {
<div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]"> <div className="border-4 border-black bg-white flex flex-col items-center justify-center min-h-[65vh] h-fit w-[50vw] rounded-3xl content-center overflow-scroll space-y-[10vh] p-[30px]">
{projects.map((project, index) => ( {projects.map((project, index) => (
<Link <Link
to={`/project/${project.id}`} to={`/project/${project.name}`}
onClick={() => { onClick={() => {
handleProjectClick(project.name); handleProjectClick(project.name);
}} }}
@ -53,7 +61,7 @@ function UserProjectPage(): JSX.Element {
const buttons = <></>; const buttons = <></>;
return <BasicWindow username="Admin" content={content} buttons={buttons} />; return <BasicWindow content={content} buttons={buttons} />;
} }
export default UserProjectPage; export default UserProjectPage;

View file

@ -29,12 +29,14 @@ import AdminProjectManageMembers from "./Pages/AdminPages/AdminProjectManageMemb
import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx"; import AdminProjectStatistics from "./Pages/AdminPages/AdminProjectStatistics.tsx";
import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx"; import AdminProjectViewMemberInfo from "./Pages/AdminPages/AdminProjectViewMemberInfo.tsx";
import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx"; import AdminProjectPage from "./Pages/AdminPages/AdminProjectPage.tsx";
import NotFoundPage from "./Pages/NotFoundPage.tsx";
// This is where the routes are mounted // This is where the routes are mounted
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
path: "/", path: "/",
element: <App />, element: <App />,
errorElement: <NotFoundPage />,
}, },
{ {
path: "/admin", path: "/admin",
@ -44,30 +46,26 @@ const router = createBrowserRouter([
path: "/pm", path: "/pm",
element: <YourProjectsPage />, element: <YourProjectsPage />,
}, },
{
path: "/user",
element: <YourProjectsPage />,
},
{ {
path: "/yourProjects", path: "/yourProjects",
element: <YourProjectsPage />, element: <YourProjectsPage />,
}, },
{ {
path: "/editTimeReport", path: "/project/:projectName",
element: <UserEditTimeReportPage />,
},
{
path: "/newTimeReport",
element: <UserNewTimeReportPage />,
},
{
path: "/project",
element: <UserProjectPage />, element: <UserProjectPage />,
}, },
{ {
path: "/projectPage", path: "/newTimeReport/:projectName",
element: <UserNewTimeReportPage />,
},
{
path: "/projectPage/:projectName",
element: <UserViewTimeReportsPage />, element: <UserViewTimeReportsPage />,
}, },
{
path: "/editTimeReport",
element: <UserEditTimeReportPage />,
},
{ {
path: "/changeRole", path: "/changeRole",
element: <PMChangeRole />, element: <PMChangeRole />,

View file

@ -1,15 +1,28 @@
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw= github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=

View file

@ -22,6 +22,27 @@ loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project" addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/submitReport" submitReportPath = base_url + "/api/submitReport"
getWeeklyReportPath = base_url + "/api/getWeeklyReport" getWeeklyReportPath = base_url + "/api/getWeeklyReport"
getProjectPath = base_url + "/api/project"
signReportPath = base_url + "/api/signReport"
addUserToProjectPath = base_url + "/api/addUserToProject"
promoteToAdminPath = base_url + "/api/promoteToAdmin"
getUserProjectsPath = base_url + "/api/getUserProjects"
def test_get_user_projects():
print("Testing get user projects")
loginResponse = login("user2", "123")
# Check if the user is added to the project
response = requests.get(
getUserProjectsPath,
json={"username": "user2"},
headers={"Authorization": "Bearer " + loginResponse.json()["token"]},
)
print(response.text)
print(response.json())
assert response.status_code == 200, "Get user projects failed"
print("got user projects successfully")
# Posts the username and password to the register endpoint # Posts the username and password to the register endpoint
@ -44,6 +65,7 @@ def login(username: string, password: string):
return response return response
# Test function to login
def test_login(): def test_login():
response = login(username, "always_same") response = login(username, "always_same")
assert response.status_code == 200, "Login failed" assert response.status_code == 200, "Login failed"
@ -51,12 +73,14 @@ def test_login():
return response.json()["token"] return response.json()["token"]
# Test function to create a new user
def test_create_user(): def test_create_user():
response = register(username, "always_same") response = register(username, "always_same")
assert response.status_code == 200, "Registration failed" assert response.status_code == 200, "Registration failed"
print("Registration successful") print("Registration successful")
# Test function to add a project
def test_add_project(): def test_add_project():
loginResponse = login(username, "always_same") loginResponse = login(username, "always_same")
token = loginResponse.json()["token"] token = loginResponse.json()["token"]
@ -70,6 +94,7 @@ def test_add_project():
print("Add project successful") print("Add project successful")
# Test function to submit a report
def test_submit_report(): def test_submit_report():
token = login(username, "always_same").json()["token"] token = login(username, "always_same").json()["token"]
response = requests.post( response = requests.post(
@ -90,18 +115,164 @@ def test_submit_report():
assert response.status_code == 200, "Submit report failed" assert response.status_code == 200, "Submit report failed"
print("Submit report successful") print("Submit report successful")
# Test function to get a weekly report
def test_get_weekly_report(): def test_get_weekly_report():
token = login(username, "always_same").json()["token"] token = login(username, "always_same").json()["token"]
response = requests.get( response = requests.get(
getWeeklyReportPath, getWeeklyReportPath,
headers={"Authorization": "Bearer " + token}, headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName , "week": 1} params={"username": username, "projectName": projectName, "week": 1},
)
print(response.text)
assert response.status_code == 200, "Get weekly report failed"
# Tests getting a project by id
def test_get_project():
token = login(username, "always_same").json()["token"]
response = requests.get(
getProjectPath + "/1", # Assumes that the project with id 1 exists
headers={"Authorization": "Bearer " + token},
)
print(response.text)
assert response.status_code == 200, "Get project failed"
# Test function to add a user to a project
def test_add_user_to_project():
# Log in as a site admin
admin_username = randomString()
admin_password = "admin_password"
print(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
) )
print(response.text) print(response.text)
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
print(response.text)
assert response.status_code == 200, "Promote to site admin failed"
print("Admin promoted to site admin successfully")
# Create a new user to add to the project
new_user = randomString()
register(new_user, "new_user_password")
# Add the new user to the project as a member
response = requests.put(
addUserToProjectPath,
json={"projectName": projectName, "username": new_user, "role": "member"},
headers={"Authorization": "Bearer " + admin_token},
)
print(response.text)
assert response.status_code == 200, "Add user to project failed"
print("Add user to project successful")
# Test function to sign a report
def test_sign_report():
# Create a project manager user
project_manager = randomString()
register(project_manager, "project_manager_password")
# Register an admin
admin_username = randomString()
admin_password = "admin_password2"
print(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password}
)
print(response.text)
# Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
response = requests.put(
addUserToProjectPath,
json={
"projectName": projectName,
"username": project_manager,
"role": "project_manager",
},
headers={"Authorization": "Bearer " + admin_token},
)
assert response.status_code == 200, "Add project manager to project failed"
print("Project manager added to project successfully")
# Log in as the project manager
project_manager_token = login(project_manager, "project_manager_password").json()[
"token"
]
# Submit a report for the project
token = login(username, "always_same").json()["token"]
response = requests.post(
submitReportPath,
json={
"projectName": projectName,
"week": 1,
"developmentTime": 10,
"meetingTime": 5,
"adminTime": 5,
"ownWorkTime": 10,
"studyTime": 10,
"testingTime": 10,
},
headers={"Authorization": "Bearer " + token},
)
assert response.status_code == 200, "Submit report failed"
print("Submit report successful")
# Retrieve the report ID
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName, "week": 1},
)
print(response.text)
report_id = response.json()["reportId"]
# Sign the report as the project manager
response = requests.post(
signReportPath,
json={"reportId": report_id},
headers={"Authorization": "Bearer " + project_manager_token},
)
assert response.status_code == 200, "Sign report failed"
print("Sign report successful")
# Retrieve the report ID again for confirmation
response = requests.get(
getWeeklyReportPath,
headers={"Authorization": "Bearer " + token},
params={"username": username, "projectName": projectName, "week": 1},
)
print(response.text)
if __name__ == "__main__": if __name__ == "__main__":
test_get_user_projects()
test_create_user() test_create_user()
test_login() test_login()
test_add_project() test_add_project()
test_submit_report() test_submit_report()
test_get_weekly_report() test_get_weekly_report()
test_get_project()
test_sign_report()
test_add_user_to_project()