2024-02-12 12:40:49 +01:00
package database
import (
2024-02-28 03:30:05 +01:00
"embed"
2024-03-17 20:30:55 +01:00
"errors"
2024-02-28 03:21:13 +01:00
"path/filepath"
2024-03-14 19:48:49 +01:00
"ttime/internal/types"
2024-02-12 12:40:49 +01:00
"github.com/jmoiron/sqlx"
2024-03-15 11:02:33 +01:00
_ "modernc.org/sqlite"
2024-02-12 12:40:49 +01:00
)
2024-02-29 20:33:20 +01:00
// Interface for the database
type Database interface {
2024-03-12 20:44:40 +01:00
// Insert a new user into the database, password should be hashed before calling
2024-02-29 20:33:20 +01:00
AddUser ( username string , password string ) error
2024-03-17 01:32:10 +01:00
CheckUser ( username string , password string ) bool
2024-02-29 20:33:20 +01:00
RemoveUser ( username string ) error
2024-03-07 13:21:47 +01:00
PromoteToAdmin ( username string ) error
2024-02-29 20:33:20 +01:00
GetUserId ( username string ) ( int , error )
AddProject ( name string , description string , username string ) error
2024-03-17 14:38:20 +01:00
Migrate ( ) error
2024-03-14 13:27:57 +01:00
GetProjectId ( projectname string ) ( int , error )
2024-03-16 22:47:19 +01:00
AddWeeklyReport ( projectName string , userName string , week int , developmentTime int , meetingTime int , adminTime int , ownWorkTime int , studyTime int , testingTime int ) error
2024-03-14 13:39:56 +01:00
AddUserToProject ( username string , projectname string , role string ) error
2024-03-14 13:47:04 +01:00
ChangeUserRole ( username string , projectname string , role string ) error
2024-03-14 16:01:56 +01:00
GetAllUsersProject ( projectname string ) ( [ ] UserProjectMember , error )
2024-03-14 16:25:54 +01:00
GetAllUsersApplication ( ) ( [ ] string , error )
2024-03-14 19:48:49 +01:00
GetProjectsForUser ( username string ) ( [ ] types . Project , error )
GetAllProjects ( ) ( [ ] types . Project , error )
2024-03-15 15:14:45 +01:00
GetProject ( projectId int ) ( types . Project , error )
2024-03-14 19:48:49 +01:00
GetUserRole ( username string , projectname string ) ( string , error )
2024-03-17 17:58:02 +01:00
GetWeeklyReport ( username string , projectName string , week int ) ( types . WeeklyReport , error )
2024-03-17 20:30:55 +01:00
SignWeeklyReport ( reportId int , projectManagerId int ) error
2024-03-18 13:32:55 +01:00
IsSiteAdmin ( username string ) ( bool , error )
2024-02-29 20:33:20 +01:00
}
2024-02-27 05:00:04 +01:00
// 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
}
2024-03-14 16:01:56 +01:00
type UserProjectMember struct {
Username string ` db:"username" `
UserRole string ` db:"p_role" `
}
2024-02-28 03:30:05 +01:00
//go:embed migrations
var scripts embed . FS
2024-03-14 13:39:56 +01:00
// TODO: Possibly break these out into separate files bundled with the embed package?
2024-02-27 05:00:04 +01:00
const userInsert = "INSERT INTO users (username, password) VALUES (?, ?)"
2024-03-08 07:47:32 +01:00
const projectInsert = "INSERT INTO projects (name, description, owner_user_id) SELECT ?, ?, id FROM users WHERE username = ?"
2024-03-07 13:21:47 +01:00
const promoteToAdmin = "INSERT INTO site_admin (admin_id) SELECT id FROM users WHERE username = ?"
2024-03-16 22:47:19 +01:00
const addWeeklyReport = ` WITH UserLookup AS ( SELECT id FROM users WHERE username = ? ) ,
2024-03-14 13:39:56 +01:00
ProjectLookup AS ( SELECT id FROM projects WHERE name = ? )
2024-03-16 22:47:19 +01:00
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 ) , ? , ? , ? , ? , ? , ? , ? ) ; `
2024-03-14 13:39:56 +01:00
const addUserToProject = "INSERT INTO user_roles (user_id, project_id, p_role) VALUES (?, ?, ?)" // WIP
2024-03-14 13:47:04 +01:00
const changeUserRole = "UPDATE user_roles SET p_role = ? WHERE user_id = ? AND project_id = ?"
2024-03-07 14:25:28 +01:00
2024-03-16 22:47:19 +01:00
const getProjectsForUser = ` SELECT projects . id , projects . name , projects . description , projects . owner_user_id
FROM projects JOIN user_roles ON projects . id = user_roles . project_id
JOIN users ON user_roles . user_id = users . id WHERE users . username = ? ; `
2024-03-14 19:48:49 +01:00
2024-02-27 05:00:04 +01:00
// DbConnect connects to the database
2024-02-29 20:33:20 +01:00
func DbConnect ( dbpath string ) Database {
2024-02-12 12:40:49 +01:00
// Open the database
2024-03-15 11:02:33 +01:00
db , err := sqlx . Connect ( "sqlite" , dbpath )
2024-02-12 12:40:49 +01:00
if err != nil {
panic ( err )
}
2024-02-27 05:00:04 +01:00
// Ping forces the connection to be established
2024-02-12 12:40:49 +01:00
err = db . Ping ( )
if err != nil {
panic ( err )
}
2024-02-27 05:00:04 +01:00
return & Db { db }
}
2024-03-17 01:32:10 +01:00
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
}
2024-03-15 16:57:42 +01:00
// GetProjectsForUser retrieves all projects associated with a specific user.
2024-03-14 19:48:49 +01:00
func ( d * Db ) GetProjectsForUser ( username string ) ( [ ] types . Project , error ) {
var projects [ ] types . Project
err := d . Select ( & projects , getProjectsForUser , username )
return projects , err
}
2024-03-15 16:57:42 +01:00
// GetAllProjects retrieves all projects from the database.
2024-03-14 19:48:49 +01:00
func ( d * Db ) GetAllProjects ( ) ( [ ] types . Project , error ) {
var projects [ ] types . Project
err := d . Select ( & projects , "SELECT * FROM projects" )
return projects , err
}
2024-03-15 16:57:42 +01:00
// GetProject retrieves a specific project by its ID.
2024-03-15 15:14:45 +01:00
func ( d * Db ) GetProject ( projectId int ) ( types . Project , error ) {
var project types . Project
2024-03-18 16:42:35 +01:00
err := d . Get ( & project , "SELECT * FROM projects WHERE id = ?" , projectId )
if err != nil {
println ( "Error getting project: " , err )
}
2024-03-15 15:14:45 +01:00
return project , err
}
2024-03-16 22:47:19 +01:00
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 )
2024-03-07 21:57:27 +01:00
return err
}
2024-03-14 13:27:57 +01:00
2024-03-15 16:57:42 +01:00
// AddUserToProject adds a user to a project with a specified role.
2024-03-07 23:24:54 +01:00
func ( d * Db ) AddUserToProject ( username string , projectname string , role string ) error { // WIP
var userid int
userid , err := d . GetUserId ( username )
2024-03-14 13:27:57 +01:00
if err != nil {
2024-03-07 23:24:54 +01:00
panic ( err )
2024-03-14 13:27:57 +01:00
}
2024-03-07 23:24:54 +01:00
var projectid int
projectid , err2 := d . GetProjectId ( projectname )
if err2 != nil {
panic ( err2 )
}
2024-03-14 13:27:57 +01:00
2024-03-14 13:39:56 +01:00
_ , err3 := d . Exec ( addUserToProject , userid , projectid , role )
2024-03-07 23:24:54 +01:00
return err3
2024-03-07 21:57:27 +01:00
}
2024-03-07 20:58:50 +01:00
2024-03-15 16:57:42 +01:00
// ChangeUserRole changes the role of a user within a project.
2024-03-14 13:39:56 +01:00
func ( d * Db ) ChangeUserRole ( username string , projectname string , role string ) error {
2024-03-15 16:57:42 +01:00
// Get the user ID
2024-03-07 23:24:54 +01:00
var userid int
userid , err := d . GetUserId ( username )
if err != nil {
panic ( err )
}
2024-03-07 20:58:50 +01:00
2024-03-15 16:57:42 +01:00
// Get the project ID
2024-03-07 23:24:54 +01:00
var projectid int
projectid , err2 := d . GetProjectId ( projectname )
if err2 != nil {
panic ( err2 )
}
2024-03-15 16:57:42 +01:00
// Execute the SQL query to change the user's role
2024-03-14 13:39:56 +01:00
_ , err3 := d . Exec ( changeUserRole , role , userid , projectid )
2024-03-07 23:24:54 +01:00
return err3
}
2024-03-07 20:58:50 +01:00
2024-03-15 16:57:42 +01:00
// GetUserRole retrieves the role of a user within a project.
2024-03-14 19:48:49 +01:00
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
}
2024-02-27 05:00:04 +01:00
// 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
2024-02-12 12:40:49 +01:00
}
2024-02-27 05:51:16 +01:00
2024-03-07 13:21:47 +01:00
func ( d * Db ) PromoteToAdmin ( username string ) error {
_ , err := d . Exec ( promoteToAdmin , username )
return err
}
2024-02-27 05:51:16 +01:00
func ( d * Db ) GetUserId ( username string ) ( int , error ) {
var id int
2024-03-07 23:24:54 +01:00
err := d . Get ( & id , "SELECT id FROM users WHERE username = ?" , username ) // Borde det inte vara "user" i singular
return id , err
}
2024-03-14 13:27:57 +01:00
func ( d * Db ) GetProjectId ( projectname string ) ( int , error ) {
2024-03-07 23:24:54 +01:00
var id int
2024-03-14 13:27:57 +01:00
err := d . Get ( & id , "SELECT id FROM projects WHERE name = ?" , projectname )
2024-02-27 05:51:16 +01:00
return id , err
}
2024-02-27 07:59:42 +01:00
// Creates a new project in the database, associated with a user
2024-02-27 05:51:16 +01:00
func ( d * Db ) AddProject ( name string , description string , username string ) error {
2024-02-27 07:59:42 +01:00
_ , err := d . Exec ( projectInsert , name , description , username )
2024-02-27 05:51:16 +01:00
return err
}
2024-02-28 03:21:13 +01:00
2024-03-14 16:01:56 +01:00
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
}
2024-03-14 16:25:54 +01:00
// 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
}
2024-03-17 17:58:02 +01:00
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 ,
2024-03-17 20:30:55 +01:00
testing_time ,
signed_by
2024-03-17 17:58:02 +01:00
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
}
2024-03-17 20:30:55 +01:00
// 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
}
2024-03-18 13:32:55 +01:00
// 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
}
2024-02-28 03:21:13 +01:00
// Reads a directory of migration files and applies them to the database.
// This will eventually be used on an embedded directory
2024-03-17 14:38:20 +01:00
func ( d * Db ) Migrate ( ) error {
2024-02-28 03:30:05 +01:00
// Read the embedded scripts directory
files , err := scripts . ReadDir ( "migrations" )
2024-02-28 03:21:13 +01:00
if err != nil {
return err
}
2024-03-17 14:38:20 +01:00
if len ( files ) == 0 {
println ( "No migration files found" )
return nil
}
2024-02-28 03:21:13 +01:00
tr := d . MustBegin ( )
// Iterate over each SQL file and execute it
for _ , file := range files {
if file . IsDir ( ) || filepath . Ext ( file . Name ( ) ) != ".sql" {
continue
}
2024-02-28 03:30:05 +01:00
// This is perhaps not the most elegant way to do this
2024-03-17 14:38:20 +01:00
sqlBytes , err := scripts . ReadFile ( "migrations/" + file . Name ( ) )
2024-02-28 03:21:13 +01:00
if err != nil {
return err
}
sqlQuery := string ( sqlBytes )
_ , err = tr . Exec ( sqlQuery )
if err != nil {
return err
}
}
2024-03-02 04:29:50 +01:00
if tr . Commit ( ) != nil {
return err
}
2024-02-28 03:21:13 +01:00
return nil
}