package database import ( "embed" "errors" "path/filepath" "ttime/internal/types" "github.com/jmoiron/sqlx" _ "modernc.org/sqlite" ) // Interface for the database type Database interface { // Insert a new user into the database, password should be hashed before calling AddUser(username string, password string) error CheckUser(username string, password string) bool RemoveUser(username string) error PromoteToAdmin(username string) error GetUserId(username string) (int, error) AddProject(name string, description string, username string) error Migrate() error MigrateSampleData() error GetProjectId(projectname string) (int, error) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error AddUserToProject(username string, projectname string, role string) error ChangeUserRole(username string, projectname string, role string) error GetAllUsersProject(projectname string) ([]UserProjectMember, error) GetAllUsersApplication() ([]string, error) GetProjectsForUser(username string) ([]types.Project, error) GetAllProjects() ([]types.Project, error) GetProject(projectId int) (types.Project, error) GetUserRole(username string, projectname string) (string, error) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) SignWeeklyReport(reportId int, projectManagerId int) error IsSiteAdmin(username string) (bool, error) } // This struct is a wrapper type that holds the database connection // Internally DB holds a connection pool, so it's safe for concurrent use type Db struct { *sqlx.DB } type UserProjectMember struct { Username string `db:"username"` UserRole string `db:"p_role"` } //go:embed migrations var scripts embed.FS //go:embed sample_data var sampleData embed.FS // TODO: Possibly break these out into separate files bundled with the embed package? const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)" const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?" const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?" const addWeeklyReport = `WITH UserLookup AS (SELECT id FROM users WHERE username = ?), ProjectLookup AS (SELECT id FROM projects WHERE name = ?) INSERT INTO weekly_reports (project_id, user_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time) VALUES ((SELECT id FROM ProjectLookup), (SELECT id FROM UserLookup),?, ?, ?, ?, ?, ?, ?);` const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?" const getProjectsForUser = `SELECT p.id, p.name, p.description FROM projects p JOIN user_roles ur ON p.id = ur.project_id JOIN users u ON ur.user_id = u.id WHERE u.username = ?` // DbConnect connects to the database func DbConnect(dbpath string) Database { // Open the database db, err := sqlx.Connect("sqlite", dbpath) if err != nil { panic(err) } // Ping forces the connection to be established err = db.Ping() if err != nil { panic(err) } return &Db{db} } func (d *Db) CheckUser(username string, password string) bool { var dbPassword string err := d.Get(&dbPassword, "SELECT password FROM users WHERE username = ?", username) if err != nil { return false } return dbPassword == password } // GetProjectsForUser retrieves all projects associated with a specific user. func (d *Db) GetProjectsForUser(username string) ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, getProjectsForUser, username) return projects, err } // GetAllProjects retrieves all projects from the database. func (d *Db) GetAllProjects() ([]types.Project, error) { var projects []types.Project err := d.Select(&projects, "SELECT * FROM projects") return projects, err } // GetProject retrieves a specific project by its ID. func (d *Db) GetProject(projectId int) (types.Project, error) { var project types.Project err := d.Get(&project, "SELECT * FROM projects WHERE id = ?", projectId) if err != nil { println("Error getting project: ", err) } return project, err } func (d *Db) AddWeeklyReport(projectName string, userName string, week int, developmentTime int, meetingTime int, adminTime int, ownWorkTime int, studyTime int, testingTime int) error { _, err := d.Exec(addWeeklyReport, userName, projectName, week, developmentTime, meetingTime, adminTime, ownWorkTime, studyTime, testingTime) return err } // AddUserToProject adds a user to a project with a specified role. func (d *Db) AddUserToProject(username string, projectname string, role string) error { // WIP var userid int userid, err := d.GetUserId(username) if err != nil { panic(err) } var projectid int projectid, err2 := d.GetProjectId(projectname) if err2 != nil { panic(err2) } _, err3 := d.Exec(addUserToProject, userid, projectid, role) return err3 } // ChangeUserRole changes the role of a user within a project. func (d *Db) ChangeUserRole(username string, projectname string, role string) error { // Get the user ID var userid int userid, err := d.GetUserId(username) if err != nil { panic(err) } // Get the project ID var projectid int projectid, err2 := d.GetProjectId(projectname) if err2 != nil { panic(err2) } // Execute the SQL query to change the user's role _, err3 := d.Exec(changeUserRole, role, userid, projectid) return err3 } // GetUserRole retrieves the role of a user within a project. func (d *Db) GetUserRole(username string, projectname string) (string, error) { var role string err := d.Get(&role, "SELECT p_role FROM user_roles WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?)", username, projectname) return role, err } // AddUser adds a user to the database func (d *Db) AddUser(username string, password string) error { _, err := d.Exec(userInsert, username, password) return err } // Removes a user from the database func (d *Db) RemoveUser(username string) error { _, err := d.Exec("DELETE FROM users WHERE username = ?", username) return err } func (d *Db) PromoteToAdmin(username string) error { _, err := d.Exec(promoteToAdmin, username) return err } func (d *Db) GetUserId(username string) (int, error) { var id int err := d.Get(&id, "SELECT id FROM users WHERE username = ?", username) // Borde det inte vara "user" i singular return id, err } func (d *Db) GetProjectId(projectname string) (int, error) { var id int err := d.Get(&id, "SELECT id FROM projects WHERE name = ?", projectname) return id, err } // Creates a new project in the database, associated with a user func (d *Db) AddProject(name string, description string, username string) error { tx := d.MustBegin() _, err := tx.Exec(projectInsert, name, description, username) if err != nil { tx.Rollback() return err } _, err = tx.Exec(changeUserRole, "project_manager", username, name) if err != nil { tx.Rollback() return err } tx.Commit() return err } func (d *Db) GetAllUsersProject(projectname string) ([]UserProjectMember, error) { // Define the SQL query to fetch users and their roles for a given project query := ` SELECT u.username, ur.p_role FROM users u INNER JOIN user_roles ur ON u.id = ur.user_id INNER JOIN projects p ON ur.project_id = p.id WHERE p.name = ? ` // Execute the query rows, err := d.Queryx(query, projectname) if err != nil { return nil, err } defer rows.Close() // Iterate over the rows and populate the result slice var users []UserProjectMember for rows.Next() { var user UserProjectMember if err := rows.StructScan(&user); err != nil { return nil, err } users = append(users, user) } if err := rows.Err(); err != nil { return nil, err } return users, nil } // GetAllUsersApplication retrieves all usernames from the database func (d *Db) GetAllUsersApplication() ([]string, error) { // Define the SQL query to fetch all usernames query := ` SELECT username FROM users ` // Execute the query rows, err := d.Queryx(query) if err != nil { return nil, err } defer rows.Close() // Iterate over the rows and populate the result slice var usernames []string for rows.Next() { var username string if err := rows.Scan(&username); err != nil { return nil, err } usernames = append(usernames, username) } if err := rows.Err(); err != nil { return nil, err } return usernames, nil } func (d *Db) GetWeeklyReport(username string, projectName string, week int) (types.WeeklyReport, error) { var report types.WeeklyReport query := ` SELECT report_id, user_id, project_id, week, development_time, meeting_time, admin_time, own_work_time, study_time, testing_time, signed_by FROM weekly_reports WHERE user_id = (SELECT id FROM users WHERE username = ?) AND project_id = (SELECT id FROM projects WHERE name = ?) AND week = ? ` err := d.Get(&report, query, username, projectName, week) return report, err } // SignWeeklyReport signs a weekly report by updating the signed_by field // with the provided project manager's ID, but only if the project manager // is in the same project as the report func (d *Db) SignWeeklyReport(reportId int, projectManagerId int) error { // Retrieve the project ID associated with the report var reportProjectID int err := d.Get(&reportProjectID, "SELECT project_id FROM weekly_reports WHERE report_id = ?", reportId) if err != nil { return err } // Retrieve the project ID associated with the project manager var managerProjectID int err = d.Get(&managerProjectID, "SELECT project_id FROM user_roles WHERE user_id = ? AND p_role = 'project_manager'", projectManagerId) if err != nil { return err } // Check if the project manager is in the same project as the report if reportProjectID != managerProjectID { return errors.New("project manager doesn't have permission to sign the report") } // Update the signed_by field of the specified report _, err = d.Exec("UPDATE weekly_reports SET signed_by = ? WHERE report_id = ?", projectManagerId, reportId) 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. // This will eventually be used on an embedded directory func (d *Db) Migrate() error { // Read the embedded scripts directory files, err := scripts.ReadDir("migrations") if err != nil { return err } if len(files) == 0 { println("No migration files found") return nil } 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 := scripts.ReadFile("migrations/" + 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 } // 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 }