Merge branch 'frontend' into gruppDM
This commit is contained in:
commit
2eab030212
7 changed files with 121 additions and 38 deletions
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -378,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
|
||||||
|
}
|
||||||
|
|
|
@ -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);
|
|
35
backend/internal/database/sample_data/0010_sample_data.sql
Normal file
35
backend/internal/database/sample_data/0010_sample_data.sql
Normal 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");
|
|
@ -34,29 +34,17 @@ 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
|
ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database
|
||||||
IncrementButtonCount(c *fiber.Ctx) error // For demonstration purposes
|
ListAllUsersProject(c *fiber.Ctx) error // To get a list of all users for a specific project
|
||||||
ListAllUsers(c *fiber.Ctx) error // To get a list of all users in the application database
|
ProjectRoleChange(c *fiber.Ctx) error // To change a users role in a 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// "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})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,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 +59,7 @@ func main() {
|
||||||
// Create the server
|
// Create the server
|
||||||
server := fiber.New()
|
server := fiber.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 +68,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,6 +75,7 @@ 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)
|
||||||
|
|
28
testing.py
28
testing.py
|
@ -28,6 +28,21 @@ addUserToProjectPath = base_url + "/api/addUserToProject"
|
||||||
promoteToAdminPath = base_url + "/api/promoteToAdmin"
|
promoteToAdminPath = base_url + "/api/promoteToAdmin"
|
||||||
getUserProjectsPath = base_url + "/api/getUserProjects"
|
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
|
||||||
def register(username: string, password: string):
|
def register(username: string, password: string):
|
||||||
|
@ -146,17 +161,7 @@ def test_add_user_to_project():
|
||||||
|
|
||||||
print(response.text)
|
print(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")
|
print("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():
|
||||||
|
@ -235,6 +240,7 @@ def test_sign_report():
|
||||||
|
|
||||||
|
|
||||||
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()
|
||||||
|
|
Loading…
Reference in a new issue