435 lines
12 KiB
Go
435 lines
12 KiB
Go
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
|
|
}
|