Compare commits

...

86 commits

Author SHA1 Message Date
Imbus
ab313551c9 Merge branch 'frontend' into dev 2024-03-19 03:40:24 +01:00
Imbus
2ce1837223 Merge branch 'frontend' into dev 2024-03-19 03:36:42 +01:00
al8763be
502cd67b4c Merge branch 'dev' into BumBranch 2024-03-19 02:15:33 +01:00
Imbus
4dbbee3249 Checking errors from transactions in go 2024-03-19 02:14:09 +01:00
al8763be
e3fd9f52ca Merge branch 'gruppPP' into BumBranch 2024-03-19 02:12:37 +01:00
al8763be
8081f289b5 fixed NewWeeklyReport 2024-03-19 02:11:47 +01:00
Peter KW
cdbd6ca0ce Small fixes to design 2024-03-19 01:48:01 +01:00
Imbus
7db03e8dbd Merge branch 'imbs' into dev 2024-03-19 01:43:10 +01:00
Imbus
5f88e92415 Merge branch 'imbs' of git.silversoft.se:Imbus/TTime into imbs 2024-03-19 01:40:36 +01:00
borean
3125b511bb correcting AddProject 2024-03-19 01:38:40 +01:00
Imbus
09014c6659 Better test feedback in python script 2024-03-19 01:38:39 +01:00
Imbus
e498f0ed63 Silencing python testing, optional verbose output 2024-03-19 01:38:39 +01:00
al8763be
77a24421e9 Merge branch 'gruppdm' into BumBranch 2024-03-19 01:24:55 +01:00
al8763be
de234c12f2 Merge branch 'imbs' into BumBranch 2024-03-19 01:24:38 +01:00
Imbus
68b01f2144 Merge branch 'borean' of github.com:imbus64/TTime into borean 2024-03-19 01:23:18 +01:00
borean
58d9001be3 AddProject changed so that user is promoted to projectmanager 2024-03-19 01:23:07 +01:00
Imbus
5bcca0202b Better test feedback in python script 2024-03-19 01:12:14 +01:00
borean
0217f2b512 AddProject changed so that user is promoted to projectmanager 2024-03-19 01:10:02 +01:00
pavel Hamawand
6a84b1c14d fix backbutton 2024-03-19 01:03:38 +01:00
pavel Hamawand
c072aff9da added change username button 2024-03-19 01:02:39 +01:00
al8763be
9434c31013 Merge remote-tracking branch 'origin/dev' into frontend 2024-03-19 00:48:13 +01:00
Imbus
c03be8c5d9 Path rename in python testing script 2024-03-19 00:46:28 +01:00
Davenludd
ba2bb1fc5e Merge branch 'frontend' into gruppDM 2024-03-19 00:43:25 +01:00
al8763be
847427a543 Merge remote-tracking branch 'origin/dev' into frontend 2024-03-19 00:42:32 +01:00
Imbus
b174ec8922 Rename for clarity and consistensy with TS api 2024-03-19 00:41:30 +01:00
Davenludd
b8c69fabf5 Remove unused variable and update API call in YourProjectsPage.tsx 2024-03-19 00:35:27 +01:00
pavel Hamawand
d2a8399bde minor fix 2024-03-19 00:35:15 +01:00
Imbus
2cfbcd15a7 Merge branch 'docs' of github.com:imbus64/TTime into docs 2024-03-19 00:34:15 +01:00
Samuel Högbom Aronson
9ce70e74e9 tyding up and tryinng to get tokens in docs to work 2024-03-19 00:33:38 +01:00
Samuel Högbom Aronson
3e35586bbe finished docs for user reletaded stucts and added endpoint for listallusers 2024-03-19 00:33:38 +01:00
Peter KW
b3dfbc47a4 Merge branch 'frontend' into gruppPP 2024-03-19 00:33:30 +01:00
Imbus
7e4e35f597 Silencing python testing, optional verbose output 2024-03-19 00:31:15 +01:00
Davenludd
17c30f5dd9 Merge branch 'frontend' into gruppDM 2024-03-19 00:28:12 +01:00
al8763be
d7e14f1886 quick fix for getUserProjects API 2024-03-19 00:27:35 +01:00
Samuel Högbom Aronson
8711f9a20d tyding up and tryinng to get tokens in docs to work 2024-03-19 00:27:31 +01:00
al8763be
59c4dab2e2 quick fix for getUserProjects API 2024-03-19 00:26:17 +01:00
Peter KW
d2ff2428cd Some corrections 2024-03-19 00:26:05 +01:00
Peter KW
36524e5cbb Changed so that it makes a modal for each user instead of a link 2024-03-19 00:25:37 +01:00
Davenludd
a2bc13ec22 Merge branch 'frontend' into gruppDM 2024-03-19 00:20:43 +01:00
al8763be
83f8097c2b API getUserProjects Fucked 2024-03-19 00:20:08 +01:00
Peter KW
a0759b099a Modul for viewing user info in admin manage users page 2024-03-19 00:17:29 +01:00
Peter KW
db4f869712 Delete user component 2024-03-19 00:15:42 +01:00
Davenludd
652f74884f Merge branch 'frontend' into gruppDM 2024-03-19 00:15:41 +01:00
al8763be
3e1f899414 Merge branch 'dev' into frontend 2024-03-19 00:14:55 +01:00
Peter KW
e55b380bb4 Should be able to delete users except for self now 2024-03-19 00:14:55 +01:00
Imbus
2ffbc2f9fd Merge docs -> dev 2024-03-19 00:10:42 +01:00
Imbus
fe08d01e15 Insanely verbose logging 2024-03-19 00:00:18 +01:00
Imbus
71caf37642 Resolve testing.py 2024-03-18 23:43:14 +01:00
Imbus
108850c20c Merge imbs -> dev 2024-03-18 23:40:56 +01:00
Imbus
4c297ab2ef Old unresolved merge conflict fixed 2024-03-18 23:39:02 +01:00
Imbus
0fa7558d64 Proper logging, all endpoint debug printing replaced with suitable log level 2024-03-18 23:38:50 +01:00
Davenludd
2eab030212 Merge branch 'frontend' into gruppDM 2024-03-18 23:37:48 +01:00
Davenludd
ff9eba039f Minor fixes YourProjectsPage 2024-03-18 23:36:59 +01:00
al8763be
2cd2ef9ef5 Merge branch 'frontend' of https://github.com/imbus64/TTime into frontend 2024-03-18 23:36:50 +01:00
al8763be
9056aafd2e Test for getUserProjectsAdded 2024-03-18 23:34:03 +01:00
Samuel Högbom Aronson
ad85194d4f finished docs for user reletaded stucts and added endpoint for listallusers 2024-03-18 23:21:49 +01:00
al8763be
2cff1d55f9 Fixed migration data 2024-03-18 23:08:38 +01:00
Imbus
7932350980 Exit with non-zero if migration fails 2024-03-18 23:04:08 +01:00
Davenludd
cc09eb0ead Remove duplicate import statements 2024-03-18 23:01:30 +01:00
Davenludd
8df3311f5a Remove duplicate code in UserProjectPage 2024-03-18 22:59:16 +01:00
al8763be
bed9381509 Merge branch 'BumBranch' into dev 2024-03-18 22:48:23 +01:00
al8763be
95b09a8ce7 Fixed getProjectForUsers 2024-03-18 22:46:53 +01:00
Samuel Högbom Aronson
f3c5abf4f3 added docs for loginrenew, login, regsiter 2024-03-18 22:40:51 +01:00
al8763be
a5399c9335 Samle data without query 2024-03-18 22:39:43 +01:00
al8763be
7ae6cce6b4 Sample data 2024-03-18 22:39:02 +01:00
Imbus
472940cedc Remove index from userId 2024-03-18 22:20:25 +01:00
Imbus
f5a914330f Removed userId identifier from user table, introducing numerous errors 2024-03-18 22:10:19 +01:00
Imbus
c31f145c35 Database sample data, make target and go code 2024-03-18 22:07:02 +01:00
Davenludd
f5a4c3d0e5 Merge branch 'frontend' into gruppDM 2024-03-18 22:05:50 +01:00
Davenludd
55fd42090d Remove username prop from BasicWindow component on all pages 2024-03-18 22:00:58 +01:00
Davenludd
5a4049eaf3 Remove username prop from BasicWindow component 2024-03-18 21:56:37 +01:00
Davenludd
59add3b6b3 Remove username prop from BasicWindow component 2024-03-18 21:55:47 +01:00
Davenludd
6982d21016 Add project name to URL in UserProjectPage 2024-03-18 21:55:15 +01:00
Davenludd
f16dc1722c Update project name in YourProjectsPage.tsx URL 2024-03-18 21:53:16 +01:00
Davenludd
31c5a78dae Refactor routing paths in main.tsx 2024-03-18 21:52:34 +01:00
Davenludd
93addc9870 Fixes in NewWeeklyReport component 2024-03-18 21:40:07 +01:00
Davenludd
847180cf75 Update user navigation route 2024-03-18 21:37:52 +01:00
Davenludd
b9d7e57f2c Update background image in Header component 2024-03-18 21:37:31 +01:00
Davenludd
25713443e2 Remove username prop from BasicWindow component 2024-03-18 21:33:20 +01:00
Imbus
2be4afd0e0 Correct ish swagger docstring 2024-03-18 20:05:47 +01:00
Imbus
2aade5d2fe Docs example 2024-03-18 19:59:14 +01:00
Davenludd
d64ec708a1 Minor fixes 2024-03-18 19:37:37 +01:00
Davenludd
3e9dc87100 Add NotFoundPage to handle 404 errors 2024-03-18 19:36:28 +01:00
Davenludd
a2ad2913e4 Add NotFoundPage component 2024-03-18 19:34:15 +01:00
Peter KW
83e781c877 Fixed getting the username and removed comment 2024-03-18 18:31:58 +01:00
Davenludd
531e9a0535 Fix links in UserProjectPage component 2024-03-18 17:38:45 +01:00
25 changed files with 806 additions and 179 deletions

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

@ -19,19 +19,174 @@ const docTemplate = `{
"host": "{{.Host}}", "host": "{{.Host}}",
"basePath": "{{.BasePath}}", "basePath": "{{.BasePath}}",
"paths": { "paths": {
"/api/register": { "/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": {
"post": { "post": {
"description": "Register a new user", "description": "Register a new user",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
"produces": [ "produces": [
"application/json" "text/plain"
], ],
"tags": [ "tags": [
"User" "User"
], ],
"summary": "Register a new user", "summary": "Register",
"parameters": [
{
"description": "User to register",
"name": "NewUser",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/types.NewUser"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "User added", "description": "User added",
@ -53,6 +208,102 @@ 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": {
"types.NewUser": {
"type": "object",
"properties": {
"password": {
"type": "string"
},
"username": {
"type": "string"
}
}
}
},
"securityDefinitions": {
"bererToken": {
"type": "apiKey",
"name": "Authorization",
"in": "header"
} }
}, },
"externalDocs": { "externalDocs": {

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
@ -49,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 = ?"
@ -60,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 {
@ -196,7 +201,25 @@ func (d *Db) GetProjectId(projectname string) (int, error) {
// Creates a new project in the database, associated with a user // Creates a new project in the database, associated with a user
func (d *Db) AddProject(name string, description string, username string) error { func (d *Db) AddProject(name string, description string, username string) error {
_, err := d.Exec(projectInsert, name, description, username) tx := d.MustBegin()
_, err := tx.Exec(projectInsert, name, description, username)
if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
_, err = tx.Exec(changeUserRole, "project_manager", username, name)
if err != nil {
if err := tx.Rollback(); err != nil {
return err
}
return err
}
if err := tx.Commit(); err != nil {
return err
}
return err return err
} }
@ -378,3 +401,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

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

@ -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"
) )
@ -67,37 +68,47 @@ 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 == "" { if projectID == "" {
log.Info("No project ID provided")
return c.Status(400).SendString("No project ID provided") return c.Status(400).SendString("No project ID provided")
} }
println("Getting project with ID: ", projectID) 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
println("Returning project: ", project.Name) 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)
} }
@ -111,7 +122,7 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
Role string `json:"role"` Role string `json:"role"`
} }
if err := c.BodyParser(&requestData); err != nil { if err := c.BodyParser(&requestData); err != nil {
println("Error parsing request body:", err) log.Info("Error parsing request body:", err)
return c.Status(400).SendString("Bad request") return c.Status(400).SendString("Bad request")
} }
@ -119,27 +130,27 @@ func (gs *GState) AddUserToProjectHandler(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token) user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims) claims := user.Claims.(jwt.MapClaims)
adminUsername := claims["name"].(string) adminUsername := claims["name"].(string)
println("Admin username from claims:", adminUsername) log.Info("Admin username from claims:", adminUsername)
isAdmin, err := gs.Db.IsSiteAdmin(adminUsername) isAdmin, err := gs.Db.IsSiteAdmin(adminUsername)
if err != nil { if err != nil {
println("Error checking admin status:", err) log.Info("Error checking admin status:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
if !isAdmin { if !isAdmin {
println("User is not a site admin:", adminUsername) log.Info("User is not a site admin:", adminUsername)
return c.Status(403).SendString("User is not a site admin") return c.Status(403).SendString("User is not a site admin")
} }
// Add the user to the project with the specified role // Add the user to the project with the specified role
err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role) err = gs.Db.AddUserToProject(requestData.Username, requestData.ProjectName, requestData.Role)
if err != nil { if err != nil {
println("Error adding user to project:", err) log.Info("Error adding user to project:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
// Return success message // Return success message
println("User added to project successfully:", requestData.Username) log.Info("User added to project successfully:", requestData.Username)
return c.SendStatus(fiber.StatusOK) 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,50 +17,62 @@ 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)
} }
@ -69,35 +82,33 @@ type ReportId struct {
} }
func (gs *GState) SignReport(c *fiber.Ctx) error { func (gs *GState) SignReport(c *fiber.Ctx) error {
println("Signing report...")
// 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)
projectManagerUsername := claims["name"].(string) projectManagerUsername := claims["name"].(string)
log.Info("Signing report for: ", projectManagerUsername)
// Extract report ID from the request query parameters // Extract report ID from the request query parameters
// reportID := c.Query("reportId") // reportID := c.Query("reportId")
rid := new(ReportId) rid := new(ReportId)
if err := c.BodyParser(rid); err != nil { if err := c.BodyParser(rid); err != nil {
return err return err
} }
println("Signing report for: ", rid.ReportId) log.Info("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 // Get the project manager's ID
projectManagerID, err := gs.Db.GetUserId(projectManagerUsername) 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")
} }
println("blabla", projectManagerID) 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(rid.ReportId, projectManagerID) err = gs.Db.SignWeeklyReport(rid.ReportId, projectManagerID)
if err != nil { if err != nil {
log.Info("Error signing weekly report:", err)
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }

View file

@ -1,43 +1,57 @@
package handlers package handlers
import ( import (
"fmt"
"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"
) )
// Register is a simple handler that registers a new user // Register is a simple handler that registers a new user
// //
// @Summary Register a new user // @Summary Register
// @Description Register a new user // @Description Register a new user
// @Tags User // @Tags User
// @Accept json // @Accept json
// @Produce json // @Produce plain
// @Success 200 {string} string "User added" // @Param NewUser body types.NewUser true "User to register"
// @Failure 400 {string} string "Bad request" // @Success 200 {string} string "User added"
// @Failure 500 {string} string "Internal server error" // @Failure 400 {string} string "Bad request"
// @Router /api/register [post] // @Failure 500 {string} string "Internal server error"
// @Router /register [post]
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")
} }
// This path should obviously be protected in the future // This path should obviously be protected in the future
// UserDelete deletes a user from the database // 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 { func (gs *GState) UserDelete(c *fiber.Ctx) error {
// Read from path parameters // Read from path parameters
username := c.Params("username") username := c.Params("username")
@ -46,28 +60,44 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error {
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 {
log.Info("User tried to delete another user")
return c.Status(403).SendString("You can only delete yourself") 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")
} }
// Login is a simple login handler that returns a JWT token // 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 { 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)
} }
@ -80,23 +110,36 @@ 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) println("Successfully signed token for user:", u.Username)
return c.JSON(fiber.Map{"token": t}) return c.JSON(types.Token{Token: t})
} }
// LoginRenew is a simple handler that renews the token // 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 { 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{
@ -107,23 +150,49 @@ 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)
} }
return c.JSON(fiber.Map{"token": t})
log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"])
return c.JSON(types.Token{Token: t})
} }
// ListAllUsers is a handler that returns a list of all users in the application database // 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 { 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)
} }
// @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 { func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
// Extract the username from the request body // Extract the username from the request body
var newUser types.NewUser var newUser types.NewUser
@ -132,15 +201,15 @@ func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error {
} }
username := newUser.Username username := newUser.Username
println("Promoting user to admin:", username) // Debug print log.Info("Promoting user to admin:", username) // Debug print
// Promote the user to a site admin in the database // Promote the user to a site admin in the database
if err := gs.Db.PromoteToAdmin(username); err != nil { if err := gs.Db.PromoteToAdmin(username); err != nil {
fmt.Println("Error promoting user to admin:", err) // Debug print log.Info("Error promoting user to admin:", err) // Debug print
return c.Status(500).SendString(err.Error()) return c.Status(500).SendString(err.Error())
} }
println("User promoted to admin successfully:", username) // Debug print log.Info("User promoted to admin successfully:", username) // Debug print
// Return a success message // Return a success message
return c.SendStatus(fiber.StatusOK) return c.SendStatus(fiber.StatusOK)

View file

@ -27,3 +27,8 @@ type PublicUser struct {
UserId string `json:"userId"` UserId string `json:"userId"`
Username string `json:"username"` Username string `json:"username"`
} }
// wrapper type for token
type Token struct {
Token string `json:"token"`
}

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"
@ -22,6 +23,10 @@ import (
// @license.name AGPL // @license.name AGPL
// @license.url https://www.gnu.org/licenses/agpl-3.0.html // @license.url https://www.gnu.org/licenses/agpl-3.0.html
//@securityDefinitions.apikey bererToken
//@in header
//@name Authorization
// @host localhost:8080 // @host localhost:8080
// @BasePath /api // @BasePath /api
@ -46,6 +51,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 +64,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!)
@ -69,7 +83,7 @@ func main() {
})) }))
// Protected routes (require a valid JWT bearer token authentication header) // Protected routes (require a valid JWT bearer token authentication header)
server.Post("/api/submitReport", gs.SubmitWeeklyReport) server.Post("/api/submitWeeklyReport", 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
@ -79,7 +93,7 @@ func main() {
server.Post("/api/signReport", gs.SignReport) server.Post("/api/signReport", gs.SignReport)
server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler)
server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) server.Post("/api/promoteToAdmin", gs.PromoteToAdmin)
server.Get("/api/users/all", gs.ListAllUsers)
// 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))
if err != nil { if err != nil {

View file

@ -42,10 +42,7 @@ interface API {
token: string, token: string,
): Promise<APIResponse<NewWeeklyReport>>; ): Promise<APIResponse<NewWeeklyReport>>;
/** Gets all the projects of a user*/ /** Gets all the projects of a user*/
getUserProjects( getUserProjects(token: string): Promise<APIResponse<Project[]>>;
username: string,
token: string,
): Promise<APIResponse<Project[]>>;
/** Gets a project from id*/ /** Gets a project from id*/
getProject(id: number): Promise<APIResponse<Project>>; getProject(id: number): Promise<APIResponse<Project>>;
} }
@ -150,10 +147,7 @@ export const api: API = {
} }
}, },
async getUserProjects( async getUserProjects(token: string): Promise<APIResponse<Project[]>> {
username: string,
token: string,
): Promise<APIResponse<Project[]>> {
try { try {
const response = await fetch("/api/getUserProjects", { const response = await fetch("/api/getUserProjects", {
method: "GET", method: "GET",
@ -161,7 +155,6 @@ export const api: API = {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + token, Authorization: "Bearer " + token,
}, },
body: JSON.stringify({ username }),
}); });
if (!response.ok) { if (!response.ok) {
@ -176,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",
}); });
} }
}, },

View file

@ -0,0 +1,34 @@
import { User } from "../Types/goTypes";
import { api, APIResponse } from "../API/API";
/**
* Use to remove a user from the system
* @param props - The username of user to remove
* @returns {boolean} True if removed, false if not
* @example
* const exampleUsername = "user";
* DeleteUser({ usernameToDelete: exampleUsername });
*/
function DeleteUser(props: { usernameToDelete: string }): boolean {
//console.log(props.usernameToDelete); FOR DEBUG
let removed = false;
api
.removeUser(
props.usernameToDelete,
localStorage.getItem("accessToken") ?? "",
)
.then((response: APIResponse<User>) => {
if (response.success) {
removed = true;
} else {
console.error(response.message);
}
})
.catch((error) => {
console.error("An error occurred during creation:", error);
});
return removed;
}
export default DeleteUser;

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,32 +1,31 @@
import { useState, useContext } from "react"; import { useState } from "react";
import type { NewWeeklyReport } from "../Types/goTypes"; import type { 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<number>();
const [developmentTime, setDevelopmentTime] = useState(0); const [developmentTime, setDevelopmentTime] = useState<number>();
const [meetingTime, setMeetingTime] = useState(0); const [meetingTime, setMeetingTime] = useState<number>();
const [adminTime, setAdminTime] = useState(0); const [adminTime, setAdminTime] = useState<number>();
const [ownWorkTime, setOwnWorkTime] = useState(0); const [ownWorkTime, setOwnWorkTime] = useState<number>();
const [studyTime, setStudyTime] = useState(0); const [studyTime, setStudyTime] = useState<number>();
const [testingTime, setTestingTime] = useState(0); const [testingTime, setTestingTime] = useState<number>();
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: week ?? 0,
developmentTime, developmentTime: developmentTime ?? 0,
meetingTime, meetingTime: meetingTime ?? 0,
adminTime, adminTime: adminTime ?? 0,
ownWorkTime, ownWorkTime: ownWorkTime ?? 0,
studyTime, studyTime: studyTime ?? 0,
testingTime, testingTime: testingTime ?? 0,
}; };
await api.submitWeeklyReport(newWeeklyReport, token); await api.submitWeeklyReport(newWeeklyReport, token);
@ -59,7 +58,9 @@ export default function NewWeeklyReport(): JSX.Element {
setWeek(weekNumber); setWeek(weekNumber);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
event.preventDefault(); const keyValue = event.key;
if (!/\d/.test(keyValue) && keyValue !== "Backspace")
event.preventDefault();
}} }}
onPaste={(event) => { onPaste={(event) => {
event.preventDefault(); event.preventDefault();

View file

@ -0,0 +1,54 @@
import { Link } from "react-router-dom";
import Button from "./Button";
import DeleteUser from "./DeleteUser";
import UserProjectListAdmin from "./UserProjectListAdmin";
function UserInfoModal(props: {
isVisible: boolean;
username: string;
onClose: () => void;
}): JSX.Element {
if (!props.isVisible) return <></>;
return (
<div
className="fixed inset-0 bg-black bg-opacity-30 backdrop-blur-sm
flex justify-center items-center"
>
<div className="border-4 border-black bg-white p-2 rounded-lg text-center">
<p className="font-bold text-[30px]">{props.username}</p>
<Link to="/AdminChangeUserName">
<p className="mb-[20px] hover:font-bold hover:cursor-pointer underline">
(Change Username)
</p>
</Link>
<div>
<h2 className="font-bold text-[22px] mb-[20px]">
Member of these projects:
</h2>
<div className="pr-6 pl-6">
<UserProjectListAdmin />
</div>
</div>
<div className="items-center space-x-6 pr-6 pl-6">
<Button
text={"Delete"}
onClick={function (): void {
DeleteUser({ usernameToDelete: props.username });
}}
type="button"
/>
<Button
text={"Close"}
onClick={function (): void {
props.onClose();
}}
type="button"
/>
</div>
</div>
</div>
);
}
export default UserInfoModal;

View file

@ -1,5 +1,6 @@
import { Link } from "react-router-dom"; import { useState } from "react";
import { PublicUser } from "../Types/goTypes"; import { PublicUser } from "../Types/goTypes";
import UserInfoModal from "./UserInfoModal";
/** /**
* The props for the UserProps component * The props for the UserProps component
@ -9,27 +10,52 @@ interface UserProps {
} }
/** /**
* A list of users for admin manage users page, that links admin to the right user page * A list of users for admin manage users page, that sets an onClick
* thanks to the state property * function for eact user <li> element, which displays a modul with
* @param props - The users to display * user info.
* @param props - An array of users users to display
* @returns {JSX.Element} The user list * @returns {JSX.Element} The user list
* @example * @example
* const users = [{ id: 1, userName: "Random name" }]; * const users = [{ id: 1, userName: "ExampleName" }];
* return <UserList users={users} />; * return <UserList users={users} />;
*/ */
export function UserListAdmin(props: UserProps): JSX.Element { export function UserListAdmin(props: UserProps): JSX.Element {
const [modalVisible, setModalVisible] = useState(false);
const [username, setUsername] = useState("");
const handleClick = (username: string): void => {
setUsername(username);
setModalVisible(true);
};
const handleClose = (): void => {
setUsername("");
setModalVisible(false);
};
return ( return (
<div> <>
<ul className="font-bold underline text-[30px] cursor-pointer padding"> <UserInfoModal
{props.users.map((user) => ( onClose={handleClose}
<Link to="/adminUserInfo" key={user.userId} state={user.username}> isVisible={modalVisible}
<li className="pt-5" key={user.userId}> username={username}
/>
<div>
<ul className="font-bold underline text-[30px] cursor-pointer padding">
{props.users.map((user) => (
<li
className="pt-5"
key={user.userId}
onClick={() => {
handleClick(user.username);
}}
>
{user.username} {user.username}
</li> </li>
</Link> ))}
))} </ul>
</ul> </div>
</div> </>
); );
} }

View file

@ -1,17 +1,17 @@
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { api } from "../API/API"; import { api } from "../API/API";
import { Project } from "../Types/goTypes"; import { Project } from "../Types/goTypes";
const UserProjectListAdmin: React.FC = () => { function UserProjectListAdmin(): JSX.Element {
const [projects, setProjects] = useState<Project[]>([]); const [projects, setProjects] = useState<Project[]>([]);
useEffect(() => { useEffect(() => {
const fetchProjects = async (): Promise<void> => { const fetchProjects = async (): Promise<void> => {
try { try {
const token = localStorage.getItem("accessToken") ?? ""; const token = localStorage.getItem("accessToken") ?? "";
const username = "NoUser"; // getUsernameFromContext(); // Assuming you have a function to get the username from your context // const username = props.username;
const response = await api.getUserProjects(username, token); const response = await api.getUserProjects(token);
if (response.success) { if (response.success) {
setProjects(response.data ?? []); setProjects(response.data ?? []);
} else { } else {
@ -26,18 +26,16 @@ const UserProjectListAdmin: React.FC = () => {
}, []); }, []);
return ( return (
<div> <div className="border-2 border-black bg-white p-2 rounded-lg text-center">
<h2>User Projects</h2>
<ul> <ul>
{projects.map((project) => ( {projects.map((project) => (
<li key={project.id}> <li key={project.id}>
<span>{project.name}</span> <span>{project.name}</span>
{/* Add any additional project details you want to display */}
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
); );
}; }
export default UserProjectListAdmin; export default UserProjectListAdmin;

View file

@ -1,3 +1,4 @@
import BackButton from "../../Components/BackButton";
import BasicWindow from "../../Components/BasicWindow"; import BasicWindow from "../../Components/BasicWindow";
import Button from "../../Components/Button"; import Button from "../../Components/Button";
@ -13,13 +14,7 @@ function AdminChangeUsername(): JSX.Element {
}} }}
type="button" type="button"
/> />
<Button <BackButton />
text="Back"
onClick={(): void => {
return;
}}
type="button"
/>
</> </>
); );

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

@ -11,9 +11,8 @@ function UserProjectPage(): JSX.Element {
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 token = localStorage.getItem("accessToken") ?? "";
const token = localStorage.getItem("accessToken") ?? ""; // replace with actual token const response = await api.getUserProjects(token);
const response = await api.getUserProjects(username, token);
console.log(response); console.log(response);
if (response.success) { if (response.success) {
setProjects(response.data ?? []); setProjects(response.data ?? []);
@ -36,7 +35,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);
}} }}

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

@ -2,6 +2,16 @@ import requests
import string import string
import random import random
debug_output = False
def gprint(*args, **kwargs):
print("\033[92m", *args, "\033[00m", **kwargs)
print("Running Tests...")
def dprint(*args, **kwargs):
if debug_output:
print(*args, **kwargs)
def randomString(len=10): def randomString(len=10):
"""Generate a random string of fixed length""" """Generate a random string of fixed length"""
@ -20,7 +30,7 @@ base_url = "http://localhost:8080"
registerPath = base_url + "/api/register" registerPath = base_url + "/api/register"
loginPath = base_url + "/api/login" loginPath = base_url + "/api/login"
addProjectPath = base_url + "/api/project" addProjectPath = base_url + "/api/project"
submitReportPath = base_url + "/api/submitReport" submitReportPath = base_url + "/api/submitWeeklyReport"
getWeeklyReportPath = base_url + "/api/getWeeklyReport" getWeeklyReportPath = base_url + "/api/getWeeklyReport"
getProjectPath = base_url + "/api/project" getProjectPath = base_url + "/api/project"
signReportPath = base_url + "/api/signReport" signReportPath = base_url + "/api/signReport"
@ -29,37 +39,56 @@ promoteToAdminPath = base_url + "/api/promoteToAdmin"
getUserProjectsPath = base_url + "/api/getUserProjects" getUserProjectsPath = base_url + "/api/getUserProjects"
def test_get_user_projects():
dprint("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"]},
)
dprint(response.text)
dprint(response.json())
assert response.status_code == 200, "Get user projects failed"
gprint("test_get_user_projects successful")
# Posts the username and password to the register endpoint # Posts the username and password to the register endpoint
def register(username: string, password: string): def register(username: string, password: string):
print("Registering with username: ", username, " and password: ", password) dprint("Registering with username: ", username, " and password: ", password)
response = requests.post( response = requests.post(
registerPath, json={"username": username, "password": password} registerPath, json={"username": username, "password": password}
) )
print(response.text) dprint(response.text)
return response return response
# Posts the username and password to the login endpoint # Posts the username and password to the login endpoint
def login(username: string, password: string): def login(username: string, password: string):
print("Logging in with username: ", username, " and password: ", password) dprint("Logging in with username: ", username, " and password: ", password)
response = requests.post( response = requests.post(
loginPath, json={"username": username, "password": password} loginPath, json={"username": username, "password": password}
) )
print(response.text) dprint(response.text)
return response return response
# Test function to login # 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"
print("Login successful") dprint("Login successful")
gprint("test_login successful")
return response.json()["token"] return response.json()["token"]
# Test function to create a new user # 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") gprint("test_create_user successful")
# Test function to add a project # Test function to add a project
def test_add_project(): def test_add_project():
@ -70,9 +99,9 @@ def test_add_project():
json={"name": projectName, "description": "This is a project"}, json={"name": projectName, "description": "This is a project"},
headers={"Authorization": "Bearer " + token}, headers={"Authorization": "Bearer " + token},
) )
print(response.text) dprint(response.text)
assert response.status_code == 200, "Add project failed" assert response.status_code == 200, "Add project failed"
print("Add project successful") gprint("test_add_project successful")
# Test function to submit a report # Test function to submit a report
def test_submit_report(): def test_submit_report():
@ -91,9 +120,9 @@ def test_submit_report():
}, },
headers={"Authorization": "Bearer " + token}, headers={"Authorization": "Bearer " + token},
) )
print(response.text) dprint(response.text)
assert response.status_code == 200, "Submit report failed" assert response.status_code == 200, "Submit report failed"
print("Submit report successful") gprint("test_submit_report successful")
# Test function to get a weekly report # Test function to get a weekly report
def test_get_weekly_report(): def test_get_weekly_report():
@ -101,37 +130,47 @@ def test_get_weekly_report():
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) dprint(response.text)
assert response.status_code == 200, "Get weekly report failed" assert response.status_code == 200, "Get weekly report failed"
gprint("test_get_weekly_report successful")
# Tests getting a project by id # Tests getting a project by id
def test_get_project(): def test_get_project():
token = login(username, "always_same").json()["token"] token = login(username, "always_same").json()["token"]
response = requests.get( response = requests.get(
getProjectPath + "/1", # Assumes that the project with id 1 exists getProjectPath + "/1", # Assumes that the project with id 1 exists
headers={"Authorization": "Bearer " + token}, headers={"Authorization": "Bearer " + token},
) )
print(response.text) dprint(response.text)
assert response.status_code == 200, "Get project failed" assert response.status_code == 200, "Get project failed"
gprint("test_get_project successful")
# Test function to add a user to a project # Test function to add a user to a project
def test_add_user_to_project(): def test_add_user_to_project():
# Log in as a site admin # Log in as a site admin
admin_username = randomString() admin_username = randomString()
admin_password = "admin_password" admin_password = "admin_password"
print("Registering with username: ", admin_username, " and password: ", admin_password) dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post( response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password} registerPath, json={"username": admin_username, "password": admin_password}
) )
print(response.text) dprint(response.text)
admin_token = login(admin_username, admin_password).json()["token"] admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(promoteToAdminPath, json={"username": admin_username}, headers={"Authorization": "Bearer " + admin_token}) response = requests.post(
print(response.text) promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
dprint(response.text)
assert response.status_code == 200, "Promote to site admin failed" assert response.status_code == 200, "Promote to site admin failed"
print("Admin promoted to site admin successfully") dprint("Admin promoted to site admin successfully")
# Create a new user to add to the project # Create a new user to add to the project
new_user = randomString() new_user = randomString()
@ -144,19 +183,9 @@ def test_add_user_to_project():
headers={"Authorization": "Bearer " + admin_token}, headers={"Authorization": "Bearer " + admin_token},
) )
print(response.text) dprint(response.text)
assert response.status_code == 200, "Add user to project failed" assert response.status_code == 200, "Add user to project failed"
print("Add user to project successful") gprint("test_add_user_to_project successful")
# Check if the user is added to the project
response = requests.get(
getUserProjectsPath,
json={"username": new_user},
headers={"Authorization": "Bearer " + admin_token},
)
print(response.text)
assert response.status_code == 200, "Get user projects failed"
print("got user projects successfully")
# Test function to sign a report # Test function to sign a report
def test_sign_report(): def test_sign_report():
@ -167,26 +196,38 @@ def test_sign_report():
# Register an admin # Register an admin
admin_username = randomString() admin_username = randomString()
admin_password = "admin_password2" admin_password = "admin_password2"
print("Registering with username: ", admin_username, " and password: ", admin_password) dprint(
"Registering with username: ", admin_username, " and password: ", admin_password
)
response = requests.post( response = requests.post(
registerPath, json={"username": admin_username, "password": admin_password} registerPath, json={"username": admin_username, "password": admin_password}
) )
print(response.text) dprint(response.text)
# Log in as the admin # Log in as the admin
admin_token = login(admin_username, admin_password).json()["token"] admin_token = login(admin_username, admin_password).json()["token"]
response = requests.post(promoteToAdminPath, json={"username": admin_username}, headers={"Authorization": "Bearer " + admin_token}) response = requests.post(
promoteToAdminPath,
json={"username": admin_username},
headers={"Authorization": "Bearer " + admin_token},
)
response = requests.put( response = requests.put(
addUserToProjectPath, addUserToProjectPath,
json={"projectName": projectName, "username": project_manager, "role": "project_manager"}, json={
"projectName": projectName,
"username": project_manager,
"role": "project_manager",
},
headers={"Authorization": "Bearer " + admin_token}, headers={"Authorization": "Bearer " + admin_token},
) )
assert response.status_code == 200, "Add project manager to project failed" assert response.status_code == 200, "Add project manager to project failed"
print("Project manager added to project successfully") dprint("Project manager added to project successfully")
# Log in as the project manager # Log in as the project manager
project_manager_token = login(project_manager, "project_manager_password").json()["token"] project_manager_token = login(project_manager, "project_manager_password").json()[
"token"
]
# Submit a report for the project # Submit a report for the project
token = login(username, "always_same").json()["token"] token = login(username, "always_same").json()["token"]
@ -205,15 +246,15 @@ def test_sign_report():
headers={"Authorization": "Bearer " + token}, headers={"Authorization": "Bearer " + token},
) )
assert response.status_code == 200, "Submit report failed" assert response.status_code == 200, "Submit report failed"
print("Submit report successful") dprint("Submit report successful")
# Retrieve the report ID # Retrieve the report ID
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) dprint(response.text)
report_id = response.json()["reportId"] report_id = response.json()["reportId"]
# Sign the report as the project manager # Sign the report as the project manager
@ -223,18 +264,20 @@ def test_sign_report():
headers={"Authorization": "Bearer " + project_manager_token}, headers={"Authorization": "Bearer " + project_manager_token},
) )
assert response.status_code == 200, "Sign report failed" assert response.status_code == 200, "Sign report failed"
print("Sign report successful") dprint("Sign report successful")
# Retrieve the report ID again for confirmation # Retrieve the report ID again for confirmation
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) dprint(response.text)
gprint("test_sign_report successful")
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()