2024-02-12 12:40:49 +01:00
package database
import (
2024-02-28 03:30:05 +01:00
"embed"
2024-02-12 12:40:49 +01:00
"os"
2024-02-28 03:21:13 +01:00
"path/filepath"
2024-03-07 21:57:27 +01:00
"time"
2024-03-14 19:48:49 +01:00
"ttime/internal/types"
2024-02-12 12:40:49 +01:00
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
)
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
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
Migrate ( dirname string ) error
2024-03-14 13:27:57 +01:00
GetProjectId ( projectname string ) ( int , error )
2024-03-14 13:39:56 +01:00
AddTimeReport ( projectName string , userName string , start time . Time , end time . Time ) error
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 )
GetUserRole ( username string , projectname string ) ( string , 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-14 13:39:56 +01:00
const addTimeReport = ` WITH UserLookup AS ( SELECT id FROM users WHERE username = ? ) ,
ProjectLookup AS ( SELECT id FROM projects WHERE name = ? )
INSERT INTO time_reports ( project_id , user_id , start , end )
VALUES ( ( SELECT id FROM ProjectLookup ) , ( SELECT id FROM UserLookup ) , ? , ? ) ; `
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-14 19:48:49 +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-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
db , err := sqlx . Connect ( "sqlite3" , dbpath )
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-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
}
func ( d * Db ) GetAllProjects ( ) ( [ ] types . Project , error ) {
var projects [ ] types . Project
err := d . Select ( & projects , "SELECT * FROM projects" )
return projects , err
}
2024-03-14 13:39:56 +01:00
func ( d * Db ) AddTimeReport ( projectName string , userName string , start time . Time , end time . Time ) error { // WIP
_ , err := d . Exec ( addTimeReport , userName , projectName , start , end )
2024-03-07 21:57:27 +01:00
return err
}
2024-03-14 13:27:57 +01:00
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-14 13:39:56 +01:00
func ( d * Db ) ChangeUserRole ( username string , projectname string , role string ) error {
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-07 23:24:54 +01:00
var projectid int
projectid , err2 := d . GetProjectId ( projectname )
if err2 != nil {
panic ( err2 )
}
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-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-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
func ( d * Db ) Migrate ( dirname string ) 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
}
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
sqlFile := filepath . Join ( "migrations" , file . Name ( ) )
2024-02-28 03:21:13 +01:00
sqlBytes , err := os . ReadFile ( sqlFile )
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
}