Enhance Sqlite DB and OAuth Session Management
- Integrated SQL tags into DBTables and other database structs, streamlining type mapping and table creation. - eplaced static table creation methods with a dynamic, reflection-based approach in getTableNames. - Optimized database backup and checksum generation. - Removed redundant code and files (bootstrapdb.go, userdb.go), centralizing table creation logic in pkgsqlite. - Fine-tuned the checkAdmin SQL query for more accurate admin identification. - Modernized OAuth session management, capturing browser info and centralizing session operations in the database layer.
- commit
- 8ddda7d505f0a4555a08c651186b98a2888e3f49
- author
- bnema · · 2023-10-13 19:12 UTC
- parent
- 47b4efc
- signature
- unsigned
10 files · +327 -199
M internal/app/app.go +10 -4
| @@ -65,10 +65,10 @@ type ContainerEngineConfig struct { | |||
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | 67 | type DBTables struct { | |
| 68 | - | User db.User | |
| 69 | - | Account db.Account | |
| 70 | - | Session db.Session | |
| 71 | - | Provider db.Provider | |
| 68 | + | User db.User `sql:"user"` | |
| 69 | + | Account db.Account `sql:"account"` | |
| 70 | + | Sessions db.Sessions `sql:"sessions"` | |
| 71 | + | Provider db.Provider `sql:"provider"` | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | 74 | func LoadConfig(config *Config) (*Config, error) { | |
| @@ -131,6 +131,12 @@ func NewApp() *App { | |||
| 131 | 131 | Config: *config, | |
| 132 | 132 | } | |
| 133 | 133 | ||
| 134 | + | // DBTables: DBTables{ | |
| 135 | + | // User: db.User{}, | |
| 136 | + | // Account: db.Account{}, | |
| 137 | + | // Sessions: db.Sessions{}, | |
| 138 | + | // Provider: db.Provider{}, | |
| 139 | + | ||
| 134 | 140 | OauthCallbackURL = config.GenerateOauthCallbackURL() | |
| 135 | 141 | return a | |
| 136 | 142 | } | |
| @@ -65,10 +65,10 @@ type ContainerEngineConfig struct { | |||||
| 65 | } | 65 | } | ||
| 66 | 66 | ||||
| 67 | type DBTables struct { | 67 | type DBTables struct { | ||
| 68 | - | User db.User | 68 | + | User db.User `sql:"user"` |
| 69 | - | Account db.Account | 69 | + | Account db.Account `sql:"account"` |
| 70 | - | Session db.Session | 70 | + | Sessions db.Sessions `sql:"sessions"` |
| 71 | - | Provider db.Provider | 71 | + | Provider db.Provider `sql:"provider"` |
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | func LoadConfig(config *Config) (*Config, error) { | 74 | func LoadConfig(config *Config) (*Config, error) { | ||
| @@ -131,6 +131,12 @@ func NewApp() *App { | |||||
| 131 | Config: *config, | 131 | Config: *config, | ||
| 132 | } | 132 | } | ||
| 133 | 133 | ||||
| 134 | + | // DBTables: DBTables{ | |||
| 135 | + | // User: db.User{}, | |||
| 136 | + | // Account: db.Account{}, | |||
| 137 | + | // Sessions: db.Sessions{}, | |||
| 138 | + | // Provider: db.Provider{}, | |||
| 139 | + | ||||
| 134 | OauthCallbackURL = config.GenerateOauthCallbackURL() | 140 | OauthCallbackURL = config.GenerateOauthCallbackURL() | ||
| 135 | return a | 141 | return a | ||
| 136 | } | 142 | } | ||
M internal/app/sqlite_test.go +13 -7
| @@ -1,9 +1,11 @@ | |||
| 1 | - | package app | |
| 1 | + | package app_test | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "database/sql" | |
| 5 | 5 | "os" | |
| 6 | 6 | "testing" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/gordon/internal/app" | |
| 7 | 9 | ) | |
| 8 | 10 | ||
| 9 | 11 | // test for CloseAndBackupDB with database modifications (checksum should be different and backup file should be created) | |
| @@ -21,8 +23,9 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||
| 21 | 23 | } | |
| 22 | 24 | tempFile.Close() | |
| 23 | 25 | ||
| 24 | - | app := &App{ | |
| 26 | + | app := &app.App{ | |
| 25 | 27 | DBFilename: tempFile.Name(), | |
| 28 | + | DBTables: app.DBTables{}, | |
| 26 | 29 | } | |
| 27 | 30 | ||
| 28 | 31 | // Initialize in-memory database | |
| @@ -30,6 +33,9 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||
| 30 | 33 | if err != nil { | |
| 31 | 34 | t.Fatalf("Failed to open in-memory database: %v", err) | |
| 32 | 35 | } | |
| 36 | + | ||
| 37 | + | // Initialize the DB field in the App struct | |
| 38 | + | app.DB = memDb | |
| 33 | 39 | defer memDb.Close() | |
| 34 | 40 | ||
| 35 | 41 | // Create table and insert some data | |
| @@ -43,13 +49,13 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||
| 43 | 49 | } | |
| 44 | 50 | ||
| 45 | 51 | // Generate initial checksum | |
| 46 | - | initialChecksum, err := GenerateDBChecksum(memDb) | |
| 52 | + | initialChecksum, err := app.GenerateDBChecksum(memDb, app) | |
| 47 | 53 | if err != nil { | |
| 48 | 54 | t.Fatalf("Failed to generate initial checksum: %v", err) | |
| 49 | 55 | } | |
| 50 | 56 | ||
| 51 | 57 | // Validate: Checksum should remain the same | |
| 52 | - | finalChecksum, err := GenerateDBChecksum(memDb) | |
| 58 | + | finalChecksum, err := GenerateDBChecksum(memDb, app) | |
| 53 | 59 | if err != nil { | |
| 54 | 60 | t.Fatalf("Failed to generate final checksum: %v", err) | |
| 55 | 61 | } | |
| @@ -64,7 +70,7 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||
| 64 | 70 | } | |
| 65 | 71 | ||
| 66 | 72 | // Validate: Checksum should be different | |
| 67 | - | finalChecksum, err = GenerateDBChecksum(memDb) | |
| 73 | + | finalChecksum, err = GenerateDBChecksum(memDb, app) | |
| 68 | 74 | if err != nil { | |
| 69 | 75 | t.Fatalf("Failed to generate final checksum: %v", err) | |
| 70 | 76 | } | |
| @@ -117,13 +123,13 @@ func TestCloseAndBackupDBWithNoModifications(t *testing.T) { | |||
| 117 | 123 | } | |
| 118 | 124 | ||
| 119 | 125 | // Generate initial checksum | |
| 120 | - | initialChecksum, err := GenerateDBChecksum(memDb) | |
| 126 | + | initialChecksum, err := GenerateDBChecksum(memDb, app) | |
| 121 | 127 | if err != nil { | |
| 122 | 128 | t.Fatalf("Failed to generate initial checksum: %v", err) | |
| 123 | 129 | } | |
| 124 | 130 | ||
| 125 | 131 | // Validate: Checksum should remain the same | |
| 126 | - | finalChecksum, err := GenerateDBChecksum(memDb) | |
| 132 | + | finalChecksum, err := GenerateDBChecksum(memDb, app) | |
| 127 | 133 | if err != nil { | |
| 128 | 134 | t.Fatalf("Failed to generate final checksum: %v", err) | |
| 129 | 135 | } | |
| @@ -1,9 +1,11 @@ | |||||
| 1 | - | package app | 1 | + | package app_test |
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | "database/sql" | 4 | "database/sql" | ||
| 5 | "os" | 5 | "os" | ||
| 6 | "testing" | 6 | "testing" | ||
| 7 | + | ||||
| 8 | + | "github.com/bnema/gordon/internal/app" | |||
| 7 | ) | 9 | ) | ||
| 8 | 10 | ||||
| 9 | // test for CloseAndBackupDB with database modifications (checksum should be different and backup file should be created) | 11 | // test for CloseAndBackupDB with database modifications (checksum should be different and backup file should be created) | ||
| @@ -21,8 +23,9 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||||
| 21 | } | 23 | } | ||
| 22 | tempFile.Close() | 24 | tempFile.Close() | ||
| 23 | 25 | ||||
| 24 | - | app := &App{ | 26 | + | app := &app.App{ |
| 25 | DBFilename: tempFile.Name(), | 27 | DBFilename: tempFile.Name(), | ||
| 28 | + | DBTables: app.DBTables{}, | |||
| 26 | } | 29 | } | ||
| 27 | 30 | ||||
| 28 | // Initialize in-memory database | 31 | // Initialize in-memory database | ||
| @@ -30,6 +33,9 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||||
| 30 | if err != nil { | 33 | if err != nil { | ||
| 31 | t.Fatalf("Failed to open in-memory database: %v", err) | 34 | t.Fatalf("Failed to open in-memory database: %v", err) | ||
| 32 | } | 35 | } | ||
| 36 | + | ||||
| 37 | + | // Initialize the DB field in the App struct | |||
| 38 | + | app.DB = memDb | |||
| 33 | defer memDb.Close() | 39 | defer memDb.Close() | ||
| 34 | 40 | ||||
| 35 | // Create table and insert some data | 41 | // Create table and insert some data | ||
| @@ -43,13 +49,13 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||||
| 43 | } | 49 | } | ||
| 44 | 50 | ||||
| 45 | // Generate initial checksum | 51 | // Generate initial checksum | ||
| 46 | - | initialChecksum, err := GenerateDBChecksum(memDb) | 52 | + | initialChecksum, err := app.GenerateDBChecksum(memDb, app) |
| 47 | if err != nil { | 53 | if err != nil { | ||
| 48 | t.Fatalf("Failed to generate initial checksum: %v", err) | 54 | t.Fatalf("Failed to generate initial checksum: %v", err) | ||
| 49 | } | 55 | } | ||
| 50 | 56 | ||||
| 51 | // Validate: Checksum should remain the same | 57 | // Validate: Checksum should remain the same | ||
| 52 | - | finalChecksum, err := GenerateDBChecksum(memDb) | 58 | + | finalChecksum, err := GenerateDBChecksum(memDb, app) |
| 53 | if err != nil { | 59 | if err != nil { | ||
| 54 | t.Fatalf("Failed to generate final checksum: %v", err) | 60 | t.Fatalf("Failed to generate final checksum: %v", err) | ||
| 55 | } | 61 | } | ||
| @@ -64,7 +70,7 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) { | |||||
| 64 | } | 70 | } | ||
| 65 | 71 | ||||
| 66 | // Validate: Checksum should be different | 72 | // Validate: Checksum should be different | ||
| 67 | - | finalChecksum, err = GenerateDBChecksum(memDb) | 73 | + | finalChecksum, err = GenerateDBChecksum(memDb, app) |
| 68 | if err != nil { | 74 | if err != nil { | ||
| 69 | t.Fatalf("Failed to generate final checksum: %v", err) | 75 | t.Fatalf("Failed to generate final checksum: %v", err) | ||
| 70 | } | 76 | } | ||
| @@ -117,13 +123,13 @@ func TestCloseAndBackupDBWithNoModifications(t *testing.T) { | |||||
| 117 | } | 123 | } | ||
| 118 | 124 | ||||
| 119 | // Generate initial checksum | 125 | // Generate initial checksum | ||
| 120 | - | initialChecksum, err := GenerateDBChecksum(memDb) | 126 | + | initialChecksum, err := GenerateDBChecksum(memDb, app) |
| 121 | if err != nil { | 127 | if err != nil { | ||
| 122 | t.Fatalf("Failed to generate initial checksum: %v", err) | 128 | t.Fatalf("Failed to generate initial checksum: %v", err) | ||
| 123 | } | 129 | } | ||
| 124 | 130 | ||||
| 125 | // Validate: Checksum should remain the same | 131 | // Validate: Checksum should remain the same | ||
| 126 | - | finalChecksum, err := GenerateDBChecksum(memDb) | 132 | + | finalChecksum, err := GenerateDBChecksum(memDb, app) |
| 127 | if err != nil { | 133 | if err != nil { | ||
| 128 | t.Fatalf("Failed to generate final checksum: %v", err) | 134 | t.Fatalf("Failed to generate final checksum: %v", err) | ||
| 129 | } | 135 | } | ||
M internal/app/sqliteclose.go +24 -19
| @@ -4,36 +4,36 @@ import ( | |||
| 4 | 4 | "database/sql" | |
| 5 | 5 | "fmt" | |
| 6 | 6 | "os" | |
| 7 | + | "reflect" | |
| 7 | 8 | ) | |
| 8 | 9 | ||
| 9 | 10 | // GetTableNames returns a list of table names in the database. | |
| 10 | - | func GetTableNames(db *sql.DB) ([]string, error) { | |
| 11 | - | rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'") | |
| 12 | - | if err != nil { | |
| 13 | - | return nil, err | |
| 14 | - | } | |
| 15 | - | defer rows.Close() | |
| 16 | - | ||
| 17 | - | var tables []string | |
| 18 | - | for rows.Next() { | |
| 19 | - | var tableName string | |
| 20 | - | if err := rows.Scan(&tableName); err != nil { | |
| 21 | - | return nil, err | |
| 11 | + | func getTableNames(a *App) []string { | |
| 12 | + | var tableNames []string | |
| 13 | + | v := reflect.ValueOf(a.DBTables) | |
| 14 | + | t := v.Type() | |
| 15 | + | for i := 0; i < v.NumField(); i++ { | |
| 16 | + | field := t.Field(i) | |
| 17 | + | sqlTag := field.Tag.Get("sql") | |
| 18 | + | if sqlTag != "" { | |
| 19 | + | tableNames = append(tableNames, sqlTag) | |
| 20 | + | } else { | |
| 21 | + | // Use the field name as a fallback | |
| 22 | + | tableNames = append(tableNames, field.Name) | |
| 22 | 23 | } | |
| 23 | - | tables = append(tables, tableName) | |
| 24 | 24 | } | |
| 25 | - | ||
| 26 | - | return tables, nil | |
| 25 | + | return tableNames | |
| 27 | 26 | } | |
| 28 | 27 | ||
| 29 | 28 | // CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification. | |
| 30 | 29 | func CloseAndBackupDB(a *App, memDb *sql.DB) error { | |
| 31 | - | currentChecksum, err := GenerateDBChecksum(memDb) | |
| 30 | + | currentChecksum, err := GenerateDBChecksum(memDb, a) | |
| 32 | 31 | if err != nil { | |
| 33 | 32 | return err | |
| 34 | 33 | } | |
| 35 | 34 | fmt.Printf("Current checksum: %s\n", currentChecksum) | |
| 36 | 35 | fmt.Printf("Initial checksum: %s\n", a.InitialChecksum) | |
| 36 | + | ||
| 37 | 37 | if currentChecksum != a.InitialChecksum { | |
| 38 | 38 | fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum) | |
| 39 | 39 | ||
| @@ -50,9 +50,14 @@ func CloseAndBackupDB(a *App, memDb *sql.DB) error { | |||
| 50 | 50 | return err | |
| 51 | 51 | } | |
| 52 | 52 | ||
| 53 | - | _, err = memDb.Exec("CREATE TABLE diskdb.users AS SELECT * FROM main.users") | |
| 54 | - | if err != nil { | |
| 55 | - | return err | |
| 53 | + | tables := getTableNames(a) | |
| 54 | + | ||
| 55 | + | for _, table := range tables { | |
| 56 | + | query := fmt.Sprintf("CREATE TABLE diskdb.%s AS SELECT * FROM main.%s", table, table) | |
| 57 | + | _, err = memDb.Exec(query) | |
| 58 | + | if err != nil { | |
| 59 | + | return err | |
| 60 | + | } | |
| 56 | 61 | } | |
| 57 | 62 | ||
| 58 | 63 | _, err = memDb.Exec("DETACH DATABASE diskdb") | |
| @@ -4,36 +4,36 @@ import ( | |||||
| 4 | "database/sql" | 4 | "database/sql" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "os" | 6 | "os" | ||
| 7 | + | "reflect" | |||
| 7 | ) | 8 | ) | ||
| 8 | 9 | ||||
| 9 | // GetTableNames returns a list of table names in the database. | 10 | // GetTableNames returns a list of table names in the database. | ||
| 10 | - | func GetTableNames(db *sql.DB) ([]string, error) { | 11 | + | func getTableNames(a *App) []string { |
| 11 | - | rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table'") | 12 | + | var tableNames []string |
| 12 | - | if err != nil { | 13 | + | v := reflect.ValueOf(a.DBTables) |
| 13 | - | return nil, err | 14 | + | t := v.Type() |
| 14 | - | } | 15 | + | for i := 0; i < v.NumField(); i++ { |
| 15 | - | defer rows.Close() | 16 | + | field := t.Field(i) |
| 16 | - | 17 | + | sqlTag := field.Tag.Get("sql") | |
| 17 | - | var tables []string | 18 | + | if sqlTag != "" { |
| 18 | - | for rows.Next() { | 19 | + | tableNames = append(tableNames, sqlTag) |
| 19 | - | var tableName string | 20 | + | } else { |
| 20 | - | if err := rows.Scan(&tableName); err != nil { | 21 | + | // Use the field name as a fallback |
| 21 | - | return nil, err | 22 | + | tableNames = append(tableNames, field.Name) |
| 22 | } | 23 | } | ||
| 23 | - | tables = append(tables, tableName) | |||
| 24 | } | 24 | } | ||
| 25 | - | 25 | + | return tableNames | |
| 26 | - | return tables, nil | |||
| 27 | } | 26 | } | ||
| 28 | 27 | ||||
| 29 | // CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification. | 28 | // CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification. | ||
| 30 | func CloseAndBackupDB(a *App, memDb *sql.DB) error { | 29 | func CloseAndBackupDB(a *App, memDb *sql.DB) error { | ||
| 31 | - | currentChecksum, err := GenerateDBChecksum(memDb) | 30 | + | currentChecksum, err := GenerateDBChecksum(memDb, a) |
| 32 | if err != nil { | 31 | if err != nil { | ||
| 33 | return err | 32 | return err | ||
| 34 | } | 33 | } | ||
| 35 | fmt.Printf("Current checksum: %s\n", currentChecksum) | 34 | fmt.Printf("Current checksum: %s\n", currentChecksum) | ||
| 36 | fmt.Printf("Initial checksum: %s\n", a.InitialChecksum) | 35 | fmt.Printf("Initial checksum: %s\n", a.InitialChecksum) | ||
| 36 | + | ||||
| 37 | if currentChecksum != a.InitialChecksum { | 37 | if currentChecksum != a.InitialChecksum { | ||
| 38 | fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum) | 38 | fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum) | ||
| 39 | 39 | ||||
| @@ -50,9 +50,14 @@ func CloseAndBackupDB(a *App, memDb *sql.DB) error { | |||||
| 50 | return err | 50 | return err | ||
| 51 | } | 51 | } | ||
| 52 | 52 | ||||
| 53 | - | _, err = memDb.Exec("CREATE TABLE diskdb.users AS SELECT * FROM main.users") | 53 | + | tables := getTableNames(a) |
| 54 | - | if err != nil { | 54 | + | |
| 55 | - | return err | 55 | + | for _, table := range tables { |
| 56 | + | query := fmt.Sprintf("CREATE TABLE diskdb.%s AS SELECT * FROM main.%s", table, table) | |||
| 57 | + | _, err = memDb.Exec(query) | |||
| 58 | + | if err != nil { | |||
| 59 | + | return err | |||
| 60 | + | } | |||
| 56 | } | 61 | } | ||
| 57 | 62 | ||||
| 58 | _, err = memDb.Exec("DETACH DATABASE diskdb") | 63 | _, err = memDb.Exec("DETACH DATABASE diskdb") | ||
M internal/app/sqliteinit.go +12 -16
| @@ -9,7 +9,7 @@ import ( | |||
| 9 | 9 | "os" | |
| 10 | 10 | "path/filepath" | |
| 11 | 11 | ||
| 12 | - | "github.com/bnema/gordon/internal/db/migrate" | |
| 12 | + | pkgsqlite "github.com/bnema/gordon/pkg/sqlite" | |
| 13 | 13 | "github.com/mattn/go-sqlite3" | |
| 14 | 14 | ) | |
| 15 | 15 | ||
| @@ -37,7 +37,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||
| 37 | 37 | } | |
| 38 | 38 | ||
| 39 | 39 | if !dbExists { | |
| 40 | - | if err := bootstrapDB(dbPath); err != nil { | |
| 40 | + | if err := bootstrapDB(dbPath, a); err != nil { | |
| 41 | 41 | return nil, fmt.Errorf("failed to bootstrap DB: %w", err) | |
| 42 | 42 | } | |
| 43 | 43 | } | |
| @@ -46,7 +46,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||
| 46 | 46 | return nil, fmt.Errorf("failed to load DB to memory: %w", err) | |
| 47 | 47 | } | |
| 48 | 48 | // Here we can generate the initial checksum | |
| 49 | - | a.InitialChecksum, err = GenerateDBChecksum(memDb) | |
| 49 | + | a.InitialChecksum, err = GenerateDBChecksum(memDb, a) | |
| 50 | 50 | if err != nil { | |
| 51 | 51 | return nil, fmt.Errorf("failed to generate initial checksum: %w", err) | |
| 52 | 52 | } | |
| @@ -76,25 +76,25 @@ func (a *App) checkDBFile(dbPath string) (bool, error) { | |||
| 76 | 76 | return true, nil | |
| 77 | 77 | } | |
| 78 | 78 | ||
| 79 | - | // bootstrapDB executes the SQL statements to create the database tables. | |
| 80 | - | func bootstrapDB(dbPath string) error { | |
| 79 | + | func bootstrapDB(dbPath string, app *App) error { | |
| 81 | 80 | db, err := sql.Open("sqlite3", dbPath) | |
| 82 | 81 | if err != nil { | |
| 83 | 82 | return err | |
| 84 | 83 | } | |
| 85 | 84 | defer db.Close() | |
| 86 | 85 | ||
| 87 | - | if err := migrate.CreateUserTable(db); err != nil { | |
| 86 | + | tables := app.DBTables | |
| 87 | + | ||
| 88 | + | if err := pkgsqlite.CreateTable(db, tables.User, "user"); err != nil { | |
| 88 | 89 | return err | |
| 89 | 90 | } | |
| 90 | - | if err := migrate.CreateAccountTable(db); err != nil { | |
| 91 | + | if err := pkgsqlite.CreateTable(db, tables.Account, "account"); err != nil { | |
| 91 | 92 | return err | |
| 92 | 93 | } | |
| 93 | - | if err := migrate.CreateSessionTable(db); err != nil { | |
| 94 | + | if err := pkgsqlite.CreateTable(db, tables.Sessions, "sessions"); err != nil { | |
| 94 | 95 | return err | |
| 95 | 96 | } | |
| 96 | - | ||
| 97 | - | if err := migrate.CreateProviderTable(db); err != nil { | |
| 97 | + | if err := pkgsqlite.CreateTable(db, tables.Provider, "provider"); err != nil { | |
| 98 | 98 | return err | |
| 99 | 99 | } | |
| 100 | 100 | ||
| @@ -165,12 +165,8 @@ func loadDBToMemory(dbPath string) (*sql.DB, error) { | |||
| 165 | 165 | } | |
| 166 | 166 | ||
| 167 | 167 | // GenerateDBChecksum generates a checksum of the database. | |
| 168 | - | func GenerateDBChecksum(db *sql.DB) (string, error) { | |
| 169 | - | tables, err := GetTableNames(db) | |
| 170 | - | if err != nil { | |
| 171 | - | return "", err | |
| 172 | - | } | |
| 173 | - | ||
| 168 | + | func GenerateDBChecksum(db *sql.DB, a *App) (string, error) { | |
| 169 | + | tables := getTableNames(a) | |
| 174 | 170 | var finalChecksum []byte | |
| 175 | 171 | ||
| 176 | 172 | for _, table := range tables { | |
| @@ -9,7 +9,7 @@ import ( | |||||
| 9 | "os" | 9 | "os" | ||
| 10 | "path/filepath" | 10 | "path/filepath" | ||
| 11 | 11 | ||||
| 12 | - | "github.com/bnema/gordon/internal/db/migrate" | 12 | + | pkgsqlite "github.com/bnema/gordon/pkg/sqlite" |
| 13 | "github.com/mattn/go-sqlite3" | 13 | "github.com/mattn/go-sqlite3" | ||
| 14 | ) | 14 | ) | ||
| 15 | 15 | ||||
| @@ -37,7 +37,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||||
| 37 | } | 37 | } | ||
| 38 | 38 | ||||
| 39 | if !dbExists { | 39 | if !dbExists { | ||
| 40 | - | if err := bootstrapDB(dbPath); err != nil { | 40 | + | if err := bootstrapDB(dbPath, a); err != nil { |
| 41 | return nil, fmt.Errorf("failed to bootstrap DB: %w", err) | 41 | return nil, fmt.Errorf("failed to bootstrap DB: %w", err) | ||
| 42 | } | 42 | } | ||
| 43 | } | 43 | } | ||
| @@ -46,7 +46,7 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||||
| 46 | return nil, fmt.Errorf("failed to load DB to memory: %w", err) | 46 | return nil, fmt.Errorf("failed to load DB to memory: %w", err) | ||
| 47 | } | 47 | } | ||
| 48 | // Here we can generate the initial checksum | 48 | // Here we can generate the initial checksum | ||
| 49 | - | a.InitialChecksum, err = GenerateDBChecksum(memDb) | 49 | + | a.InitialChecksum, err = GenerateDBChecksum(memDb, a) |
| 50 | if err != nil { | 50 | if err != nil { | ||
| 51 | return nil, fmt.Errorf("failed to generate initial checksum: %w", err) | 51 | return nil, fmt.Errorf("failed to generate initial checksum: %w", err) | ||
| 52 | } | 52 | } | ||
| @@ -76,25 +76,25 @@ func (a *App) checkDBFile(dbPath string) (bool, error) { | |||||
| 76 | return true, nil | 76 | return true, nil | ||
| 77 | } | 77 | } | ||
| 78 | 78 | ||||
| 79 | - | // bootstrapDB executes the SQL statements to create the database tables. | 79 | + | func bootstrapDB(dbPath string, app *App) error { |
| 80 | - | func bootstrapDB(dbPath string) error { | |||
| 81 | db, err := sql.Open("sqlite3", dbPath) | 80 | db, err := sql.Open("sqlite3", dbPath) | ||
| 82 | if err != nil { | 81 | if err != nil { | ||
| 83 | return err | 82 | return err | ||
| 84 | } | 83 | } | ||
| 85 | defer db.Close() | 84 | defer db.Close() | ||
| 86 | 85 | ||||
| 87 | - | if err := migrate.CreateUserTable(db); err != nil { | 86 | + | tables := app.DBTables |
| 87 | + | ||||
| 88 | + | if err := pkgsqlite.CreateTable(db, tables.User, "user"); err != nil { | |||
| 88 | return err | 89 | return err | ||
| 89 | } | 90 | } | ||
| 90 | - | if err := migrate.CreateAccountTable(db); err != nil { | 91 | + | if err := pkgsqlite.CreateTable(db, tables.Account, "account"); err != nil { |
| 91 | return err | 92 | return err | ||
| 92 | } | 93 | } | ||
| 93 | - | if err := migrate.CreateSessionTable(db); err != nil { | 94 | + | if err := pkgsqlite.CreateTable(db, tables.Sessions, "sessions"); err != nil { |
| 94 | return err | 95 | return err | ||
| 95 | } | 96 | } | ||
| 96 | - | 97 | + | if err := pkgsqlite.CreateTable(db, tables.Provider, "provider"); err != nil { | |
| 97 | - | if err := migrate.CreateProviderTable(db); err != nil { | |||
| 98 | return err | 98 | return err | ||
| 99 | } | 99 | } | ||
| 100 | 100 | ||||
| @@ -165,12 +165,8 @@ func loadDBToMemory(dbPath string) (*sql.DB, error) { | |||||
| 165 | } | 165 | } | ||
| 166 | 166 | ||||
| 167 | // GenerateDBChecksum generates a checksum of the database. | 167 | // GenerateDBChecksum generates a checksum of the database. | ||
| 168 | - | func GenerateDBChecksum(db *sql.DB) (string, error) { | 168 | + | func GenerateDBChecksum(db *sql.DB, a *App) (string, error) { |
| 169 | - | tables, err := GetTableNames(db) | 169 | + | tables := getTableNames(a) |
| 170 | - | if err != nil { | |||
| 171 | - | return "", err | |||
| 172 | - | } | |||
| 173 | - | ||||
| 174 | var finalChecksum []byte | 170 | var finalChecksum []byte | ||
| 175 | 171 | ||||
| 176 | for _, table := range tables { | 172 | for _, table := range tables { | ||
D internal/db/migrate/bootstrapdb.go +0 -81
| @@ -1,81 +0,0 @@ | |||
| 1 | - | package migrate | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "database/sql" | |
| 5 | - | "fmt" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | // CreateUserTable creates the 'users' table. | |
| 9 | - | func CreateUserTable(db *sql.DB) error { | |
| 10 | - | createTableSQL := ` | |
| 11 | - | CREATE TABLE user ( | |
| 12 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 13 | - | name TEXT NOT NULL, | |
| 14 | - | email TEXT NOT NULL UNIQUE | |
| 15 | - | );` | |
| 16 | - | ||
| 17 | - | _, err := db.Exec(createTableSQL) | |
| 18 | - | if err != nil { | |
| 19 | - | return fmt.Errorf("failed to create user table: %v", err) | |
| 20 | - | } | |
| 21 | - | ||
| 22 | - | return nil | |
| 23 | - | } | |
| 24 | - | ||
| 25 | - | // CreateAccountTable creates the 'accounts' table. | |
| 26 | - | func CreateAccountTable(db *sql.DB) error { | |
| 27 | - | createTableSQL := ` | |
| 28 | - | CREATE TABLE account ( | |
| 29 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 30 | - | user_id INTEGER UNIQUE, | |
| 31 | - | FOREIGN KEY (user_id) REFERENCES user(id) | |
| 32 | - | );` | |
| 33 | - | ||
| 34 | - | _, err := db.Exec(createTableSQL) | |
| 35 | - | if err != nil { | |
| 36 | - | return fmt.Errorf("failed to create account table: %v", err) | |
| 37 | - | } | |
| 38 | - | ||
| 39 | - | return nil | |
| 40 | - | } | |
| 41 | - | ||
| 42 | - | // CreateSessionTable creates the 'sessions' table. | |
| 43 | - | func CreateSessionTable(db *sql.DB) error { | |
| 44 | - | createTableSQL := ` | |
| 45 | - | CREATE TABLE session ( | |
| 46 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 47 | - | account_id INTEGER, | |
| 48 | - | session_token TEXT NOT NULL, | |
| 49 | - | expires TEXT NOT NULL, | |
| 50 | - | is_online BOOLEAN, | |
| 51 | - | FOREIGN KEY (account_id) REFERENCES account(id) | |
| 52 | - | );` | |
| 53 | - | ||
| 54 | - | _, err := db.Exec(createTableSQL) | |
| 55 | - | if err != nil { | |
| 56 | - | return fmt.Errorf("failed to create session table: %v", err) | |
| 57 | - | } | |
| 58 | - | ||
| 59 | - | return nil | |
| 60 | - | } | |
| 61 | - | ||
| 62 | - | // CreateProviderTable creates the 'providers' table. | |
| 63 | - | func CreateProviderTable(db *sql.DB) error { | |
| 64 | - | createTableSQL := ` | |
| 65 | - | CREATE TABLE provider ( | |
| 66 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |
| 67 | - | account_id INTEGER, | |
| 68 | - | name TEXT NOT NULL, | |
| 69 | - | access_token TEXT NOT NULL, | |
| 70 | - | refresh_token TEXT, | |
| 71 | - | expires TEXT NOT NULL, | |
| 72 | - | FOREIGN KEY (account_id) REFERENCES account(id) | |
| 73 | - | );` | |
| 74 | - | ||
| 75 | - | _, err := db.Exec(createTableSQL) | |
| 76 | - | if err != nil { | |
| 77 | - | return fmt.Errorf("failed to create provider table: %v", err) | |
| 78 | - | } | |
| 79 | - | ||
| 80 | - | return nil | |
| 81 | - | } | |
| @@ -1,81 +0,0 @@ | |||||
| 1 | - | package migrate | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "database/sql" | |||
| 5 | - | "fmt" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | // CreateUserTable creates the 'users' table. | |||
| 9 | - | func CreateUserTable(db *sql.DB) error { | |||
| 10 | - | createTableSQL := ` | |||
| 11 | - | CREATE TABLE user ( | |||
| 12 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 13 | - | name TEXT NOT NULL, | |||
| 14 | - | email TEXT NOT NULL UNIQUE | |||
| 15 | - | );` | |||
| 16 | - | ||||
| 17 | - | _, err := db.Exec(createTableSQL) | |||
| 18 | - | if err != nil { | |||
| 19 | - | return fmt.Errorf("failed to create user table: %v", err) | |||
| 20 | - | } | |||
| 21 | - | ||||
| 22 | - | return nil | |||
| 23 | - | } | |||
| 24 | - | ||||
| 25 | - | // CreateAccountTable creates the 'accounts' table. | |||
| 26 | - | func CreateAccountTable(db *sql.DB) error { | |||
| 27 | - | createTableSQL := ` | |||
| 28 | - | CREATE TABLE account ( | |||
| 29 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 30 | - | user_id INTEGER UNIQUE, | |||
| 31 | - | FOREIGN KEY (user_id) REFERENCES user(id) | |||
| 32 | - | );` | |||
| 33 | - | ||||
| 34 | - | _, err := db.Exec(createTableSQL) | |||
| 35 | - | if err != nil { | |||
| 36 | - | return fmt.Errorf("failed to create account table: %v", err) | |||
| 37 | - | } | |||
| 38 | - | ||||
| 39 | - | return nil | |||
| 40 | - | } | |||
| 41 | - | ||||
| 42 | - | // CreateSessionTable creates the 'sessions' table. | |||
| 43 | - | func CreateSessionTable(db *sql.DB) error { | |||
| 44 | - | createTableSQL := ` | |||
| 45 | - | CREATE TABLE session ( | |||
| 46 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 47 | - | account_id INTEGER, | |||
| 48 | - | session_token TEXT NOT NULL, | |||
| 49 | - | expires TEXT NOT NULL, | |||
| 50 | - | is_online BOOLEAN, | |||
| 51 | - | FOREIGN KEY (account_id) REFERENCES account(id) | |||
| 52 | - | );` | |||
| 53 | - | ||||
| 54 | - | _, err := db.Exec(createTableSQL) | |||
| 55 | - | if err != nil { | |||
| 56 | - | return fmt.Errorf("failed to create session table: %v", err) | |||
| 57 | - | } | |||
| 58 | - | ||||
| 59 | - | return nil | |||
| 60 | - | } | |||
| 61 | - | ||||
| 62 | - | // CreateProviderTable creates the 'providers' table. | |||
| 63 | - | func CreateProviderTable(db *sql.DB) error { | |||
| 64 | - | createTableSQL := ` | |||
| 65 | - | CREATE TABLE provider ( | |||
| 66 | - | id INTEGER PRIMARY KEY AUTOINCREMENT, | |||
| 67 | - | account_id INTEGER, | |||
| 68 | - | name TEXT NOT NULL, | |||
| 69 | - | access_token TEXT NOT NULL, | |||
| 70 | - | refresh_token TEXT, | |||
| 71 | - | expires TEXT NOT NULL, | |||
| 72 | - | FOREIGN KEY (account_id) REFERENCES account(id) | |||
| 73 | - | );` | |||
| 74 | - | ||||
| 75 | - | _, err := db.Exec(createTableSQL) | |||
| 76 | - | if err != nil { | |||
| 77 | - | return fmt.Errorf("failed to create provider table: %v", err) | |||
| 78 | - | } | |||
| 79 | - | ||||
| 80 | - | return nil | |||
| 81 | - | } | |||
D internal/db/queries/userdb.go +0 -36
| @@ -1,36 +0,0 @@ | |||
| 1 | - | package queries | |
| 2 | - | ||
| 3 | - | import ( | |
| 4 | - | "github.com/bnema/gordon/internal/app" | |
| 5 | - | "github.com/bnema/gordon/internal/db" | |
| 6 | - | ) | |
| 7 | - | ||
| 8 | - | // Pseudo-code for all queries that will be needed on the user database | |
| 9 | - | // User table | |
| 10 | - | // User has many accounts (one to many) because a user can auth in many ways also have many sessions | |
| 11 | - | ||
| 12 | - | // 1. Create a user (one only, if id=1 then it's the admin) | |
| 13 | - | // This happens when the user logs in for the first time | |
| 14 | - | // TODO: prevent anyone from creating another user | |
| 15 | - | func CreateUser(a *app.App) error { | |
| 16 | - | ||
| 17 | - | // To create a user i need to : | |
| 18 | - | // 1. check if gordontoken == gordontoken inside the yaml config file | |
| 19 | - | // 2. check if user already exists | |
| 20 | - | // 3. get the informations from the oauth provider (only github for now) | |
| 21 | - | // 4. create the user in the database | |
| 22 | - | // 5. create the account in the database | |
| 23 | - | // 6. create the session in the database | |
| 24 | - | // 7. return the user informations (id, name, email, image, is_admin) | |
| 25 | - | return nil | |
| 26 | - | } | |
| 27 | - | ||
| 28 | - | // GetUserInformations gets the user informations based on the access token of the oauth provider | |
| 29 | - | func GetUserInformations(a *app.App, accessToken string) (db.User, error) { | |
| 30 | - | ||
| 31 | - | InspectInMemoryDB(a) | |
| 32 | - | ||
| 33 | - | user := db.User{} | |
| 34 | - | ||
| 35 | - | return user, nil | |
| 36 | - | } | |
| @@ -1,36 +0,0 @@ | |||||
| 1 | - | package queries | |||
| 2 | - | ||||
| 3 | - | import ( | |||
| 4 | - | "github.com/bnema/gordon/internal/app" | |||
| 5 | - | "github.com/bnema/gordon/internal/db" | |||
| 6 | - | ) | |||
| 7 | - | ||||
| 8 | - | // Pseudo-code for all queries that will be needed on the user database | |||
| 9 | - | // User table | |||
| 10 | - | // User has many accounts (one to many) because a user can auth in many ways also have many sessions | |||
| 11 | - | ||||
| 12 | - | // 1. Create a user (one only, if id=1 then it's the admin) | |||
| 13 | - | // This happens when the user logs in for the first time | |||
| 14 | - | // TODO: prevent anyone from creating another user | |||
| 15 | - | func CreateUser(a *app.App) error { | |||
| 16 | - | ||||
| 17 | - | // To create a user i need to : | |||
| 18 | - | // 1. check if gordontoken == gordontoken inside the yaml config file | |||
| 19 | - | // 2. check if user already exists | |||
| 20 | - | // 3. get the informations from the oauth provider (only github for now) | |||
| 21 | - | // 4. create the user in the database | |||
| 22 | - | // 5. create the account in the database | |||
| 23 | - | // 6. create the session in the database | |||
| 24 | - | // 7. return the user informations (id, name, email, image, is_admin) | |||
| 25 | - | return nil | |||
| 26 | - | } | |||
| 27 | - | ||||
| 28 | - | // GetUserInformations gets the user informations based on the access token of the oauth provider | |||
| 29 | - | func GetUserInformations(a *app.App, accessToken string) (db.User, error) { | |||
| 30 | - | ||||
| 31 | - | InspectInMemoryDB(a) | |||
| 32 | - | ||||
| 33 | - | user := db.User{} | |||
| 34 | - | ||||
| 35 | - | return user, nil | |||
| 36 | - | } | |||
A internal/db/queries/userqueries.go +138 -0
| @@ -0,0 +1,138 @@ | |||
| 1 | + | package queries | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "fmt" | |
| 5 | + | "time" | |
| 6 | + | ||
| 7 | + | "github.com/bnema/gordon/internal/app" | |
| 8 | + | "github.com/bnema/gordon/internal/db" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // CreateUser creates a new user along with the associated account, provider, and session. | |
| 12 | + | func CreateUser(a *app.App, accessToken string, browserInfo string) error { | |
| 13 | + | ||
| 14 | + | // !! Additionnal security check !! | |
| 15 | + | // Check if a user already exists. If so, return an error. | |
| 16 | + | userExists, err := UserExists(a) | |
| 17 | + | if err != nil { | |
| 18 | + | return err | |
| 19 | + | } | |
| 20 | + | if userExists { | |
| 21 | + | return fmt.Errorf("user already exists") | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | user := db.User{} | |
| 25 | + | account := db.Account{} | |
| 26 | + | // Step 1: Create an entry in the user table | |
| 27 | + | // TODO : obtain the user name and email from the GitHub API | |
| 28 | + | result, err := a.DB.Exec("INSERT INTO user (name, email) VALUES (?, ?)", "admin", "admin@gordon") | |
| 29 | + | if err != nil { | |
| 30 | + | return err | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // Get the last insert ID from the result of the INSERT operation | |
| 34 | + | user.ID, err = result.LastInsertId() | |
| 35 | + | if err != nil { | |
| 36 | + | return fmt.Errorf("error while fetching LastInsertId: %w", err) | |
| 37 | + | } | |
| 38 | + | ||
| 39 | + | fmt.Printf("User ID: %d\n", user.ID) | |
| 40 | + | ||
| 41 | + | a.DBTables.User.ID = user.ID | |
| 42 | + | ||
| 43 | + | // Step 2: Create an account for the user with the user ID | |
| 44 | + | account.ID, err = createAccount(a) | |
| 45 | + | if err != nil { | |
| 46 | + | return err | |
| 47 | + | } | |
| 48 | + | // Step 3: Create a provider for the user | |
| 49 | + | if err := createProvider(a, accessToken); err != nil { | |
| 50 | + | return err | |
| 51 | + | } | |
| 52 | + | ||
| 53 | + | // Step 4: Create a session for the user | |
| 54 | + | if err := createSession(a, browserInfo); err != nil { | |
| 55 | + | return err | |
| 56 | + | } | |
| 57 | + | ||
| 58 | + | return nil | |
| 59 | + | } | |
| 60 | + | ||
| 61 | + | func createAccount(a *app.App) (int64, error) { | |
| 62 | + | fmt.Println("Whats the value of a.DBTables.User.ID?", a.DBTables.User.ID) | |
| 63 | + | account := a.DBTables.Account | |
| 64 | + | queryInsertAccount := "INSERT INTO account (user_id) VALUES (?) RETURNING id" | |
| 65 | + | // Execute the INSERT operation | |
| 66 | + | result, err := a.DB.Exec(queryInsertAccount, a.DBTables.User.ID) | |
| 67 | + | if err != nil { | |
| 68 | + | return 0, fmt.Errorf("error while inserting into account: %w", err) | |
| 69 | + | } | |
| 70 | + | ||
| 71 | + | // Get the last inserted ID | |
| 72 | + | account.ID, err = result.LastInsertId() | |
| 73 | + | if err != nil { | |
| 74 | + | return 0, fmt.Errorf("error while fetching LastInsertId for account: %w", err) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | return account.ID, nil | |
| 78 | + | } | |
| 79 | + | ||
| 80 | + | func createProvider(a *app.App, accessToken string) error { | |
| 81 | + | queryInsertProvider := "INSERT INTO provider (account_id, name, access_token, refresh_token, expires) VALUES (?, ?, ?, ?, ?)" | |
| 82 | + | _, err := a.DB.Exec(queryInsertProvider, a.DBTables.Account.ID, "GitHub", accessToken, "refreshToken", time.Now().Add(time.Hour*24).Format(time.RFC3339)) | |
| 83 | + | return err | |
| 84 | + | } | |
| 85 | + | ||
| 86 | + | // createSession creates a session for the user. | |
| 87 | + | func createSession(a *app.App, browserInfo string) error { | |
| 88 | + | queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?)" | |
| 89 | + | _, err := a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true) | |
| 90 | + | return err | |
| 91 | + | } | |
| 92 | + | ||
| 93 | + | func CreateOrUpdateSession(a *app.App, accessToken string, browserInfo string) error { | |
| 94 | + | existingSessionID := a.DBTables.Sessions.ID | |
| 95 | + | ||
| 96 | + | queryFindExistingSession := "SELECT id FROM sessions WHERE account_id = ? AND browser_info = ?" | |
| 97 | + | err := a.DB.QueryRow(queryFindExistingSession, a.DBTables.Account.ID, browserInfo).Scan(&existingSessionID) | |
| 98 | + | if err != nil { | |
| 99 | + | return err | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | // Calculate the new expiry time | |
| 103 | + | newExpiryTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339) | |
| 104 | + | ||
| 105 | + | if existingSessionID > 0 { | |
| 106 | + | // Update the session if it exists | |
| 107 | + | _, err = a.DB.Exec("UPDATE sessions SET expires = ?, is_online = ? WHERE id = ?", newExpiryTime, true, existingSessionID) | |
| 108 | + | if err != nil { | |
| 109 | + | return err | |
| 110 | + | } | |
| 111 | + | } else { | |
| 112 | + | // Create a new session if it doesn't exist | |
| 113 | + | queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)" | |
| 114 | + | _, err = a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true) | |
| 115 | + | if err != nil { | |
| 116 | + | return err | |
| 117 | + | } | |
| 118 | + | } | |
| 119 | + | ||
| 120 | + | return nil | |
| 121 | + | } | |
| 122 | + | ||
| 123 | + | func UserExists(a *app.App) (bool, error) { | |
| 124 | + | var count int | |
| 125 | + | err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count) | |
| 126 | + | if err != nil { | |
| 127 | + | return false, err | |
| 128 | + | } | |
| 129 | + | if count > 0 { | |
| 130 | + | return true, nil | |
| 131 | + | } | |
| 132 | + | return false, nil | |
| 133 | + | } | |
| 134 | + | ||
| 135 | + | func UpdateAccessToken(a *app.App, accessToken string) error { | |
| 136 | + | _, err := a.DB.Exec("UPDATE provider SET access_token = ? WHERE name = ?", accessToken, "GitHub") | |
| 137 | + | return err | |
| 138 | + | } | |
| @@ -0,0 +1,138 @@ | |||||
| 1 | + | package queries | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "fmt" | |||
| 5 | + | "time" | |||
| 6 | + | ||||
| 7 | + | "github.com/bnema/gordon/internal/app" | |||
| 8 | + | "github.com/bnema/gordon/internal/db" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // CreateUser creates a new user along with the associated account, provider, and session. | |||
| 12 | + | func CreateUser(a *app.App, accessToken string, browserInfo string) error { | |||
| 13 | + | ||||
| 14 | + | // !! Additionnal security check !! | |||
| 15 | + | // Check if a user already exists. If so, return an error. | |||
| 16 | + | userExists, err := UserExists(a) | |||
| 17 | + | if err != nil { | |||
| 18 | + | return err | |||
| 19 | + | } | |||
| 20 | + | if userExists { | |||
| 21 | + | return fmt.Errorf("user already exists") | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | user := db.User{} | |||
| 25 | + | account := db.Account{} | |||
| 26 | + | // Step 1: Create an entry in the user table | |||
| 27 | + | // TODO : obtain the user name and email from the GitHub API | |||
| 28 | + | result, err := a.DB.Exec("INSERT INTO user (name, email) VALUES (?, ?)", "admin", "admin@gordon") | |||
| 29 | + | if err != nil { | |||
| 30 | + | return err | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // Get the last insert ID from the result of the INSERT operation | |||
| 34 | + | user.ID, err = result.LastInsertId() | |||
| 35 | + | if err != nil { | |||
| 36 | + | return fmt.Errorf("error while fetching LastInsertId: %w", err) | |||
| 37 | + | } | |||
| 38 | + | ||||
| 39 | + | fmt.Printf("User ID: %d\n", user.ID) | |||
| 40 | + | ||||
| 41 | + | a.DBTables.User.ID = user.ID | |||
| 42 | + | ||||
| 43 | + | // Step 2: Create an account for the user with the user ID | |||
| 44 | + | account.ID, err = createAccount(a) | |||
| 45 | + | if err != nil { | |||
| 46 | + | return err | |||
| 47 | + | } | |||
| 48 | + | // Step 3: Create a provider for the user | |||
| 49 | + | if err := createProvider(a, accessToken); err != nil { | |||
| 50 | + | return err | |||
| 51 | + | } | |||
| 52 | + | ||||
| 53 | + | // Step 4: Create a session for the user | |||
| 54 | + | if err := createSession(a, browserInfo); err != nil { | |||
| 55 | + | return err | |||
| 56 | + | } | |||
| 57 | + | ||||
| 58 | + | return nil | |||
| 59 | + | } | |||
| 60 | + | ||||
| 61 | + | func createAccount(a *app.App) (int64, error) { | |||
| 62 | + | fmt.Println("Whats the value of a.DBTables.User.ID?", a.DBTables.User.ID) | |||
| 63 | + | account := a.DBTables.Account | |||
| 64 | + | queryInsertAccount := "INSERT INTO account (user_id) VALUES (?) RETURNING id" | |||
| 65 | + | // Execute the INSERT operation | |||
| 66 | + | result, err := a.DB.Exec(queryInsertAccount, a.DBTables.User.ID) | |||
| 67 | + | if err != nil { | |||
| 68 | + | return 0, fmt.Errorf("error while inserting into account: %w", err) | |||
| 69 | + | } | |||
| 70 | + | ||||
| 71 | + | // Get the last inserted ID | |||
| 72 | + | account.ID, err = result.LastInsertId() | |||
| 73 | + | if err != nil { | |||
| 74 | + | return 0, fmt.Errorf("error while fetching LastInsertId for account: %w", err) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | return account.ID, nil | |||
| 78 | + | } | |||
| 79 | + | ||||
| 80 | + | func createProvider(a *app.App, accessToken string) error { | |||
| 81 | + | queryInsertProvider := "INSERT INTO provider (account_id, name, access_token, refresh_token, expires) VALUES (?, ?, ?, ?, ?)" | |||
| 82 | + | _, err := a.DB.Exec(queryInsertProvider, a.DBTables.Account.ID, "GitHub", accessToken, "refreshToken", time.Now().Add(time.Hour*24).Format(time.RFC3339)) | |||
| 83 | + | return err | |||
| 84 | + | } | |||
| 85 | + | ||||
| 86 | + | // createSession creates a session for the user. | |||
| 87 | + | func createSession(a *app.App, browserInfo string) error { | |||
| 88 | + | queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?)" | |||
| 89 | + | _, err := a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true) | |||
| 90 | + | return err | |||
| 91 | + | } | |||
| 92 | + | ||||
| 93 | + | func CreateOrUpdateSession(a *app.App, accessToken string, browserInfo string) error { | |||
| 94 | + | existingSessionID := a.DBTables.Sessions.ID | |||
| 95 | + | ||||
| 96 | + | queryFindExistingSession := "SELECT id FROM sessions WHERE account_id = ? AND browser_info = ?" | |||
| 97 | + | err := a.DB.QueryRow(queryFindExistingSession, a.DBTables.Account.ID, browserInfo).Scan(&existingSessionID) | |||
| 98 | + | if err != nil { | |||
| 99 | + | return err | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | // Calculate the new expiry time | |||
| 103 | + | newExpiryTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339) | |||
| 104 | + | ||||
| 105 | + | if existingSessionID > 0 { | |||
| 106 | + | // Update the session if it exists | |||
| 107 | + | _, err = a.DB.Exec("UPDATE sessions SET expires = ?, is_online = ? WHERE id = ?", newExpiryTime, true, existingSessionID) | |||
| 108 | + | if err != nil { | |||
| 109 | + | return err | |||
| 110 | + | } | |||
| 111 | + | } else { | |||
| 112 | + | // Create a new session if it doesn't exist | |||
| 113 | + | queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)" | |||
| 114 | + | _, err = a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true) | |||
| 115 | + | if err != nil { | |||
| 116 | + | return err | |||
| 117 | + | } | |||
| 118 | + | } | |||
| 119 | + | ||||
| 120 | + | return nil | |||
| 121 | + | } | |||
| 122 | + | ||||
| 123 | + | func UserExists(a *app.App) (bool, error) { | |||
| 124 | + | var count int | |||
| 125 | + | err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count) | |||
| 126 | + | if err != nil { | |||
| 127 | + | return false, err | |||
| 128 | + | } | |||
| 129 | + | if count > 0 { | |||
| 130 | + | return true, nil | |||
| 131 | + | } | |||
| 132 | + | return false, nil | |||
| 133 | + | } | |||
| 134 | + | ||||
| 135 | + | func UpdateAccessToken(a *app.App, accessToken string) error { | |||
| 136 | + | _, err := a.DB.Exec("UPDATE provider SET access_token = ? WHERE name = ?", accessToken, "GitHub") | |||
| 137 | + | return err | |||
| 138 | + | } | |||
M internal/db/struct.go +18 -18
| @@ -1,32 +1,32 @@ | |||
| 1 | 1 | package db | |
| 2 | 2 | ||
| 3 | 3 | type User struct { | |
| 4 | - | ID int64 | |
| 5 | - | Name string | |
| 6 | - | Email string | |
| 4 | + | ID int64 `sql:"id, primary_key"` | |
| 5 | + | Name string `sql:"name"` | |
| 6 | + | Email string `sql:"email"` | |
| 7 | 7 | Account Account | |
| 8 | 8 | } | |
| 9 | 9 | ||
| 10 | 10 | type Account struct { | |
| 11 | - | ID int64 | |
| 12 | - | UserID int64 | |
| 13 | - | Sessions []Session | |
| 11 | + | ID int64 `sql:"id, primary_key"` | |
| 12 | + | UserID int64 `sql:"user_id, foreign_key=user.id"` | |
| 13 | + | Sessions []Sessions | |
| 14 | 14 | Providers []Provider | |
| 15 | 15 | } | |
| 16 | 16 | ||
| 17 | - | type Session struct { | |
| 18 | - | ID int64 | |
| 19 | - | AccountID int64 | |
| 20 | - | SessionToken string | |
| 21 | - | Expires string | |
| 22 | - | IsOnline bool | |
| 17 | + | type Sessions struct { | |
| 18 | + | ID int64 `sql:"id, primary_key"` | |
| 19 | + | AccountID int64 `sql:"account_id, foreign_key=account.id"` | |
| 20 | + | BrowserInfo string `sql:"browser_info"` | |
| 21 | + | Expires string `sql:"expires"` | |
| 22 | + | IsOnline bool `sql:"is_online"` | |
| 23 | 23 | } | |
| 24 | 24 | ||
| 25 | 25 | type Provider struct { | |
| 26 | - | ID int64 | |
| 27 | - | AccountID int64 | |
| 28 | - | Name string | |
| 29 | - | AccessToken string | |
| 30 | - | RefreshToken string | |
| 31 | - | Expires string | |
| 26 | + | ID int64 `sql:"id, primary_key"` | |
| 27 | + | AccountID int64 `sql:"account_id, foreign_key=account.id"` | |
| 28 | + | Name string `sql:"name"` | |
| 29 | + | AccessToken string `sql:"access_token"` | |
| 30 | + | RefreshToken string `sql:"refresh_token"` | |
| 31 | + | Expires string `sql:"expires"` | |
| 32 | 32 | } | |
| @@ -1,32 +1,32 @@ | |||||
| 1 | package db | 1 | package db | ||
| 2 | 2 | ||||
| 3 | type User struct { | 3 | type User struct { | ||
| 4 | - | ID int64 | 4 | + | ID int64 `sql:"id, primary_key"` |
| 5 | - | Name string | 5 | + | Name string `sql:"name"` |
| 6 | - | Email string | 6 | + | Email string `sql:"email"` |
| 7 | Account Account | 7 | Account Account | ||
| 8 | } | 8 | } | ||
| 9 | 9 | ||||
| 10 | type Account struct { | 10 | type Account struct { | ||
| 11 | - | ID int64 | 11 | + | ID int64 `sql:"id, primary_key"` |
| 12 | - | UserID int64 | 12 | + | UserID int64 `sql:"user_id, foreign_key=user.id"` |
| 13 | - | Sessions []Session | 13 | + | Sessions []Sessions |
| 14 | Providers []Provider | 14 | Providers []Provider | ||
| 15 | } | 15 | } | ||
| 16 | 16 | ||||
| 17 | - | type Session struct { | 17 | + | type Sessions struct { |
| 18 | - | ID int64 | 18 | + | ID int64 `sql:"id, primary_key"` |
| 19 | - | AccountID int64 | 19 | + | AccountID int64 `sql:"account_id, foreign_key=account.id"` |
| 20 | - | SessionToken string | 20 | + | BrowserInfo string `sql:"browser_info"` |
| 21 | - | Expires string | 21 | + | Expires string `sql:"expires"` |
| 22 | - | IsOnline bool | 22 | + | IsOnline bool `sql:"is_online"` |
| 23 | } | 23 | } | ||
| 24 | 24 | ||||
| 25 | type Provider struct { | 25 | type Provider struct { | ||
| 26 | - | ID int64 | 26 | + | ID int64 `sql:"id, primary_key"` |
| 27 | - | AccountID int64 | 27 | + | AccountID int64 `sql:"account_id, foreign_key=account.id"` |
| 28 | - | Name string | 28 | + | Name string `sql:"name"` |
| 29 | - | AccessToken string | 29 | + | AccessToken string `sql:"access_token"` |
| 30 | - | RefreshToken string | 30 | + | RefreshToken string `sql:"refresh_token"` |
| 31 | - | Expires string | 31 | + | Expires string `sql:"expires"` |
| 32 | } | 32 | } | ||
M internal/httpserve/handler/oauth.go +34 -18
| @@ -3,11 +3,11 @@ package handler | |||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/base64" | |
| 5 | 5 | "fmt" | |
| 6 | - | "log" | |
| 7 | 6 | "net/http" | |
| 8 | 7 | "os" | |
| 9 | 8 | ||
| 10 | 9 | "github.com/bnema/gordon/internal/app" | |
| 10 | + | "github.com/bnema/gordon/internal/db" | |
| 11 | 11 | "github.com/bnema/gordon/internal/db/queries" | |
| 12 | 12 | "github.com/bnema/gordon/internal/templates/render" | |
| 13 | 13 | _ "github.com/joho/godotenv/autoload" | |
| @@ -28,7 +28,7 @@ func compareGordonToken(c echo.Context, a *app.App) error { | |||
| 28 | 28 | // Check if there is already a user in the db which means that the admin is already setup | |
| 29 | 29 | func checkAdmin(a *app.App) (bool, error) { | |
| 30 | 30 | var count int | |
| 31 | - | err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count) | |
| 31 | + | err := a.DB.QueryRow("SELECT COUNT(*) FROM user WHERE id IS NOT NULL AND name != '' AND email != ''").Scan(&count) | |
| 32 | 32 | if err != nil { | |
| 33 | 33 | return false, err | |
| 34 | 34 | } | |
| @@ -93,42 +93,58 @@ func StartOAuthGithub(c echo.Context, a *app.App) error { | |||
| 93 | 93 | return c.Redirect(http.StatusFound, oauthURL) | |
| 94 | 94 | } | |
| 95 | 95 | ||
| 96 | + | type Sessions struct { | |
| 97 | + | *db.Sessions | |
| 98 | + | } | |
| 99 | + | ||
| 96 | 100 | func OAuthCallback(c echo.Context, a *app.App) error { | |
| 97 | 101 | ||
| 98 | 102 | accessToken := c.QueryParam("access_token") | |
| 99 | 103 | encodedState := c.QueryParam("state") | |
| 100 | 104 | ||
| 101 | - | // Decode the state parameter to get the original redirectDomain | |
| 105 | + | // Decode the state parameter to validate the original redirectDomain | |
| 102 | 106 | _, err := base64.StdEncoding.DecodeString(encodedState) | |
| 103 | 107 | if err != nil { | |
| 104 | 108 | return c.String(http.StatusBadRequest, "Invalid state parameter") | |
| 105 | 109 | } | |
| 106 | 110 | ||
| 107 | - | // Get the user information from the OAuth provider | |
| 108 | - | userInfo, err := queries.GetUserInformations(a, accessToken) | |
| 109 | - | if err != nil { | |
| 110 | - | log.Printf("Failed to get user information: %v\n", err) | |
| 111 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user information") | |
| 112 | - | } | |
| 113 | - | ||
| 114 | - | fmt.Printf("User info: %+v\n", userInfo) | |
| 115 | - | ||
| 116 | - | // If the user does not exist, create it | |
| 117 | - | ||
| 118 | - | // If the user exists, update the access_token for this provider | |
| 119 | - | ||
| 120 | - | // Also, you can save the authentication state in the session | |
| 121 | 111 | sess, err := session.Get("session", c) | |
| 122 | 112 | if err != nil { | |
| 123 | 113 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | |
| 124 | 114 | } | |
| 125 | - | ||
| 126 | 115 | // Set the user as authenticated | |
| 127 | 116 | sess.Values["authenticated"] = true | |
| 128 | 117 | sess.Values["access_token"] = accessToken | |
| 118 | + | sess.Values["expires"] = 604800 | |
| 129 | 119 | if err = sess.Save(c.Request(), c.Response()); err != nil { | |
| 130 | 120 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session") | |
| 131 | 121 | } | |
| 132 | 122 | ||
| 123 | + | browserInfo := c.Request().UserAgent() | |
| 124 | + | ||
| 125 | + | // Check if the user already exists in the database | |
| 126 | + | userExists, err := queries.UserExists(a) | |
| 127 | + | if err != nil { | |
| 128 | + | return err | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // If the user does not exist, create it | |
| 132 | + | if !userExists { | |
| 133 | + | err := queries.CreateUser(a, accessToken, browserInfo) | |
| 134 | + | if err != nil { | |
| 135 | + | return err | |
| 136 | + | } | |
| 137 | + | } else { | |
| 138 | + | // If the user exists, update acccessToken and then create or update the session | |
| 139 | + | err := queries.UpdateAccessToken(a, accessToken) | |
| 140 | + | if err != nil { | |
| 141 | + | return err | |
| 142 | + | } | |
| 143 | + | ||
| 144 | + | err = queries.CreateOrUpdateSession(a, accessToken, browserInfo) | |
| 145 | + | if err != nil { | |
| 146 | + | return err | |
| 147 | + | } | |
| 148 | + | } | |
| 133 | 149 | return c.Redirect(http.StatusFound, "/admin") | |
| 134 | 150 | } | |
| @@ -3,11 +3,11 @@ package handler | |||||
| 3 | import ( | 3 | import ( | ||
| 4 | "encoding/base64" | 4 | "encoding/base64" | ||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | - | "log" | |||
| 7 | "net/http" | 6 | "net/http" | ||
| 8 | "os" | 7 | "os" | ||
| 9 | 8 | ||||
| 10 | "github.com/bnema/gordon/internal/app" | 9 | "github.com/bnema/gordon/internal/app" | ||
| 10 | + | "github.com/bnema/gordon/internal/db" | |||
| 11 | "github.com/bnema/gordon/internal/db/queries" | 11 | "github.com/bnema/gordon/internal/db/queries" | ||
| 12 | "github.com/bnema/gordon/internal/templates/render" | 12 | "github.com/bnema/gordon/internal/templates/render" | ||
| 13 | _ "github.com/joho/godotenv/autoload" | 13 | _ "github.com/joho/godotenv/autoload" | ||
| @@ -28,7 +28,7 @@ func compareGordonToken(c echo.Context, a *app.App) error { | |||||
| 28 | // Check if there is already a user in the db which means that the admin is already setup | 28 | // Check if there is already a user in the db which means that the admin is already setup | ||
| 29 | func checkAdmin(a *app.App) (bool, error) { | 29 | func checkAdmin(a *app.App) (bool, error) { | ||
| 30 | var count int | 30 | var count int | ||
| 31 | - | err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count) | 31 | + | err := a.DB.QueryRow("SELECT COUNT(*) FROM user WHERE id IS NOT NULL AND name != '' AND email != ''").Scan(&count) |
| 32 | if err != nil { | 32 | if err != nil { | ||
| 33 | return false, err | 33 | return false, err | ||
| 34 | } | 34 | } | ||
| @@ -93,42 +93,58 @@ func StartOAuthGithub(c echo.Context, a *app.App) error { | |||||
| 93 | return c.Redirect(http.StatusFound, oauthURL) | 93 | return c.Redirect(http.StatusFound, oauthURL) | ||
| 94 | } | 94 | } | ||
| 95 | 95 | ||||
| 96 | + | type Sessions struct { | |||
| 97 | + | *db.Sessions | |||
| 98 | + | } | |||
| 99 | + | ||||
| 96 | func OAuthCallback(c echo.Context, a *app.App) error { | 100 | func OAuthCallback(c echo.Context, a *app.App) error { | ||
| 97 | 101 | ||||
| 98 | accessToken := c.QueryParam("access_token") | 102 | accessToken := c.QueryParam("access_token") | ||
| 99 | encodedState := c.QueryParam("state") | 103 | encodedState := c.QueryParam("state") | ||
| 100 | 104 | ||||
| 101 | - | // Decode the state parameter to get the original redirectDomain | 105 | + | // Decode the state parameter to validate the original redirectDomain |
| 102 | _, err := base64.StdEncoding.DecodeString(encodedState) | 106 | _, err := base64.StdEncoding.DecodeString(encodedState) | ||
| 103 | if err != nil { | 107 | if err != nil { | ||
| 104 | return c.String(http.StatusBadRequest, "Invalid state parameter") | 108 | return c.String(http.StatusBadRequest, "Invalid state parameter") | ||
| 105 | } | 109 | } | ||
| 106 | 110 | ||||
| 107 | - | // Get the user information from the OAuth provider | |||
| 108 | - | userInfo, err := queries.GetUserInformations(a, accessToken) | |||
| 109 | - | if err != nil { | |||
| 110 | - | log.Printf("Failed to get user information: %v\n", err) | |||
| 111 | - | return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user information") | |||
| 112 | - | } | |||
| 113 | - | ||||
| 114 | - | fmt.Printf("User info: %+v\n", userInfo) | |||
| 115 | - | ||||
| 116 | - | // If the user does not exist, create it | |||
| 117 | - | ||||
| 118 | - | // If the user exists, update the access_token for this provider | |||
| 119 | - | ||||
| 120 | - | // Also, you can save the authentication state in the session | |||
| 121 | sess, err := session.Get("session", c) | 111 | sess, err := session.Get("session", c) | ||
| 122 | if err != nil { | 112 | if err != nil { | ||
| 123 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | 113 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session") | ||
| 124 | } | 114 | } | ||
| 125 | - | ||||
| 126 | // Set the user as authenticated | 115 | // Set the user as authenticated | ||
| 127 | sess.Values["authenticated"] = true | 116 | sess.Values["authenticated"] = true | ||
| 128 | sess.Values["access_token"] = accessToken | 117 | sess.Values["access_token"] = accessToken | ||
| 118 | + | sess.Values["expires"] = 604800 | |||
| 129 | if err = sess.Save(c.Request(), c.Response()); err != nil { | 119 | if err = sess.Save(c.Request(), c.Response()); err != nil { | ||
| 130 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session") | 120 | return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session") | ||
| 131 | } | 121 | } | ||
| 132 | 122 | ||||
| 123 | + | browserInfo := c.Request().UserAgent() | |||
| 124 | + | ||||
| 125 | + | // Check if the user already exists in the database | |||
| 126 | + | userExists, err := queries.UserExists(a) | |||
| 127 | + | if err != nil { | |||
| 128 | + | return err | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // If the user does not exist, create it | |||
| 132 | + | if !userExists { | |||
| 133 | + | err := queries.CreateUser(a, accessToken, browserInfo) | |||
| 134 | + | if err != nil { | |||
| 135 | + | return err | |||
| 136 | + | } | |||
| 137 | + | } else { | |||
| 138 | + | // If the user exists, update acccessToken and then create or update the session | |||
| 139 | + | err := queries.UpdateAccessToken(a, accessToken) | |||
| 140 | + | if err != nil { | |||
| 141 | + | return err | |||
| 142 | + | } | |||
| 143 | + | ||||
| 144 | + | err = queries.CreateOrUpdateSession(a, accessToken, browserInfo) | |||
| 145 | + | if err != nil { | |||
| 146 | + | return err | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 133 | return c.Redirect(http.StatusFound, "/admin") | 149 | return c.Redirect(http.StatusFound, "/admin") | ||
| 134 | } | 150 | } | ||
A pkg/sqlite/createdb.go +78 -0
| @@ -0,0 +1,78 @@ | |||
| 1 | + | package pkgsqlite | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "database/sql" | |
| 5 | + | "fmt" | |
| 6 | + | "reflect" | |
| 7 | + | "strings" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | // CreateTable creates a table in the database based on the given struct. | |
| 11 | + | func CreateTable(db *sql.DB, table interface{}, tableName string) error { | |
| 12 | + | t := reflect.TypeOf(table) | |
| 13 | + | ||
| 14 | + | var fields []string | |
| 15 | + | var foreignKeys []string | |
| 16 | + | ||
| 17 | + | for i := 0; i < t.NumField(); i++ { | |
| 18 | + | field := t.Field(i) | |
| 19 | + | sqlTag := field.Tag.Get("sql") | |
| 20 | + | if sqlTag == "" { | |
| 21 | + | continue | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | parts := strings.Split(sqlTag, ",") | |
| 25 | + | sqlName := parts[0] | |
| 26 | + | constraint := "" | |
| 27 | + | ||
| 28 | + | for _, part := range parts[1:] { | |
| 29 | + | fmt.Println("Inspecting part:", part) | |
| 30 | + | if part == "primary_key" { | |
| 31 | + | constraint = "INTEGER PRIMARY KEY AUTOINCREMENT" | |
| 32 | + | } else if strings.HasPrefix(strings.TrimSpace(part), "foreign_key=") { | |
| 33 | + | fkParts := strings.Split(part, "=") | |
| 34 | + | refTableAndField := strings.Split(fkParts[1], ".") | |
| 35 | + | if len(refTableAndField) == 1 { | |
| 36 | + | foreignKeys = append(foreignKeys, fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s", sqlName, fkParts[1])) | |
| 37 | + | } else if len(refTableAndField) == 2 { | |
| 38 | + | foreignKeys = append(foreignKeys, fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s(%s)", sqlName, refTableAndField[0], refTableAndField[1])) | |
| 39 | + | } | |
| 40 | + | fmt.Println("Entered foreign_key block") | |
| 41 | + | } | |
| 42 | + | } | |
| 43 | + | ||
| 44 | + | if constraint == "" { | |
| 45 | + | constraint = convertGoTypeToSQLType(field.Type.Name()) | |
| 46 | + | } | |
| 47 | + | ||
| 48 | + | fieldString := fmt.Sprintf("%s %s", sqlName, constraint) | |
| 49 | + | fields = append(fields, fieldString) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | createTableSQL := fmt.Sprintf("CREATE TABLE %s (%s", tableName, strings.Join(fields, ", ")) | |
| 53 | + | if len(foreignKeys) > 0 { | |
| 54 | + | createTableSQL += ", " + strings.Join(foreignKeys, ", ") | |
| 55 | + | fmt.Printf("Table %s linked with: %s\n", tableName, strings.Join(foreignKeys, ", ")) | |
| 56 | + | } | |
| 57 | + | createTableSQL += ");" | |
| 58 | + | ||
| 59 | + | _, err := db.Exec(createTableSQL) | |
| 60 | + | if err != nil { | |
| 61 | + | return fmt.Errorf("failed to create table %s: %v", tableName, err) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | return nil | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | func convertGoTypeToSQLType(goType string) string { | |
| 68 | + | switch goType { | |
| 69 | + | case "int64": | |
| 70 | + | return "INTEGER" | |
| 71 | + | case "string": | |
| 72 | + | return "TEXT NOT NULL" | |
| 73 | + | case "bool": | |
| 74 | + | return "BOOLEAN" | |
| 75 | + | default: | |
| 76 | + | return "TEXT" | |
| 77 | + | } | |
| 78 | + | } | |
| @@ -0,0 +1,78 @@ | |||||
| 1 | + | package pkgsqlite | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "database/sql" | |||
| 5 | + | "fmt" | |||
| 6 | + | "reflect" | |||
| 7 | + | "strings" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | // CreateTable creates a table in the database based on the given struct. | |||
| 11 | + | func CreateTable(db *sql.DB, table interface{}, tableName string) error { | |||
| 12 | + | t := reflect.TypeOf(table) | |||
| 13 | + | ||||
| 14 | + | var fields []string | |||
| 15 | + | var foreignKeys []string | |||
| 16 | + | ||||
| 17 | + | for i := 0; i < t.NumField(); i++ { | |||
| 18 | + | field := t.Field(i) | |||
| 19 | + | sqlTag := field.Tag.Get("sql") | |||
| 20 | + | if sqlTag == "" { | |||
| 21 | + | continue | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | parts := strings.Split(sqlTag, ",") | |||
| 25 | + | sqlName := parts[0] | |||
| 26 | + | constraint := "" | |||
| 27 | + | ||||
| 28 | + | for _, part := range parts[1:] { | |||
| 29 | + | fmt.Println("Inspecting part:", part) | |||
| 30 | + | if part == "primary_key" { | |||
| 31 | + | constraint = "INTEGER PRIMARY KEY AUTOINCREMENT" | |||
| 32 | + | } else if strings.HasPrefix(strings.TrimSpace(part), "foreign_key=") { | |||
| 33 | + | fkParts := strings.Split(part, "=") | |||
| 34 | + | refTableAndField := strings.Split(fkParts[1], ".") | |||
| 35 | + | if len(refTableAndField) == 1 { | |||
| 36 | + | foreignKeys = append(foreignKeys, fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s", sqlName, fkParts[1])) | |||
| 37 | + | } else if len(refTableAndField) == 2 { | |||
| 38 | + | foreignKeys = append(foreignKeys, fmt.Sprintf("FOREIGN KEY (%s) REFERENCES %s(%s)", sqlName, refTableAndField[0], refTableAndField[1])) | |||
| 39 | + | } | |||
| 40 | + | fmt.Println("Entered foreign_key block") | |||
| 41 | + | } | |||
| 42 | + | } | |||
| 43 | + | ||||
| 44 | + | if constraint == "" { | |||
| 45 | + | constraint = convertGoTypeToSQLType(field.Type.Name()) | |||
| 46 | + | } | |||
| 47 | + | ||||
| 48 | + | fieldString := fmt.Sprintf("%s %s", sqlName, constraint) | |||
| 49 | + | fields = append(fields, fieldString) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | createTableSQL := fmt.Sprintf("CREATE TABLE %s (%s", tableName, strings.Join(fields, ", ")) | |||
| 53 | + | if len(foreignKeys) > 0 { | |||
| 54 | + | createTableSQL += ", " + strings.Join(foreignKeys, ", ") | |||
| 55 | + | fmt.Printf("Table %s linked with: %s\n", tableName, strings.Join(foreignKeys, ", ")) | |||
| 56 | + | } | |||
| 57 | + | createTableSQL += ");" | |||
| 58 | + | ||||
| 59 | + | _, err := db.Exec(createTableSQL) | |||
| 60 | + | if err != nil { | |||
| 61 | + | return fmt.Errorf("failed to create table %s: %v", tableName, err) | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | return nil | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | func convertGoTypeToSQLType(goType string) string { | |||
| 68 | + | switch goType { | |||
| 69 | + | case "int64": | |||
| 70 | + | return "INTEGER" | |||
| 71 | + | case "string": | |||
| 72 | + | return "TEXT NOT NULL" | |||
| 73 | + | case "bool": | |||
| 74 | + | return "BOOLEAN" | |||
| 75 | + | default: | |||
| 76 | + | return "TEXT" | |||
| 77 | + | } | |||
| 78 | + | } | |||