detached 55157a1

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
55157a1a23f6cb5163b77bf61add9fd3f05e9baf
author
bnema · · 2023-10-13 19:12 UTC
parent
96c9b67
signature
unsigned
10 files · +327 -199
M internal/app/app.go +10 -4
@@ -65,10 +65,10 @@ type ContainerEngineConfig struct {
6565 }
6666
6767 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"`
7272 }
7373
7474 func LoadConfig(config *Config) (*Config, error) {
@@ -131,6 +131,12 @@ func NewApp() *App {
131131 Config: *config,
132132 }
133133
134+ // DBTables: DBTables{
135+ // User: db.User{},
136+ // Account: db.Account{},
137+ // Sessions: db.Sessions{},
138+ // Provider: db.Provider{},
139+
134140 OauthCallbackURL = config.GenerateOauthCallbackURL()
135141 return a
136142 }
@@ -65,10 +65,10 @@ type ContainerEngineConfig struct {
65 }65 }
66 66
67 type DBTables struct {67 type DBTables struct {
68- User db.User68+ User db.User `sql:"user"`
69- Account db.Account69+ Account db.Account `sql:"account"`
70- Session db.Session70+ Sessions db.Sessions `sql:"sessions"`
71- Provider db.Provider71+ 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 a141 return a
136 }142 }
M internal/app/sqlite_test.go +13 -7
@@ -1,9 +1,11 @@
1-package app
1+package app_test
22
33 import (
44 "database/sql"
55 "os"
66 "testing"
7+
8+ "github.com/bnema/gordon/internal/app"
79 )
810
911 // 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) {
2123 }
2224 tempFile.Close()
2325
24- app := &App{
26+ app := &app.App{
2527 DBFilename: tempFile.Name(),
28+ DBTables: app.DBTables{},
2629 }
2730
2831 // Initialize in-memory database
@@ -30,6 +33,9 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) {
3033 if err != nil {
3134 t.Fatalf("Failed to open in-memory database: %v", err)
3235 }
36+
37+ // Initialize the DB field in the App struct
38+ app.DB = memDb
3339 defer memDb.Close()
3440
3541 // Create table and insert some data
@@ -43,13 +49,13 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) {
4349 }
4450
4551 // Generate initial checksum
46- initialChecksum, err := GenerateDBChecksum(memDb)
52+ initialChecksum, err := app.GenerateDBChecksum(memDb, app)
4753 if err != nil {
4854 t.Fatalf("Failed to generate initial checksum: %v", err)
4955 }
5056
5157 // Validate: Checksum should remain the same
52- finalChecksum, err := GenerateDBChecksum(memDb)
58+ finalChecksum, err := GenerateDBChecksum(memDb, app)
5359 if err != nil {
5460 t.Fatalf("Failed to generate final checksum: %v", err)
5561 }
@@ -64,7 +70,7 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) {
6470 }
6571
6672 // Validate: Checksum should be different
67- finalChecksum, err = GenerateDBChecksum(memDb)
73+ finalChecksum, err = GenerateDBChecksum(memDb, app)
6874 if err != nil {
6975 t.Fatalf("Failed to generate final checksum: %v", err)
7076 }
@@ -117,13 +123,13 @@ func TestCloseAndBackupDBWithNoModifications(t *testing.T) {
117123 }
118124
119125 // Generate initial checksum
120- initialChecksum, err := GenerateDBChecksum(memDb)
126+ initialChecksum, err := GenerateDBChecksum(memDb, app)
121127 if err != nil {
122128 t.Fatalf("Failed to generate initial checksum: %v", err)
123129 }
124130
125131 // Validate: Checksum should remain the same
126- finalChecksum, err := GenerateDBChecksum(memDb)
132+ finalChecksum, err := GenerateDBChecksum(memDb, app)
127133 if err != nil {
128134 t.Fatalf("Failed to generate final checksum: %v", err)
129135 }
@@ -1,9 +1,11 @@
1-package app1+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 database31 // 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 data41 // Create table and insert some data
@@ -43,13 +49,13 @@ func TestCloseAndBackupDBWithModifications(t *testing.T) {
43 }49 }
44 50
45 // Generate initial checksum51 // 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 same57 // 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 different72 // 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 checksum125 // 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 same131 // 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 (
44 "database/sql"
55 "fmt"
66 "os"
7+ "reflect"
78 )
89
910 // 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)
2223 }
23- tables = append(tables, tableName)
2424 }
25-
26- return tables, nil
25+ return tableNames
2726 }
2827
2928 // CloseAndBackupDB closes the in-memory database and backup the database file if there is any modification.
3029 func CloseAndBackupDB(a *App, memDb *sql.DB) error {
31- currentChecksum, err := GenerateDBChecksum(memDb)
30+ currentChecksum, err := GenerateDBChecksum(memDb, a)
3231 if err != nil {
3332 return err
3433 }
3534 fmt.Printf("Current checksum: %s\n", currentChecksum)
3635 fmt.Printf("Initial checksum: %s\n", a.InitialChecksum)
36+
3737 if currentChecksum != a.InitialChecksum {
3838 fmt.Printf("Modification has been found in the database. Current checksum: %s\n", currentChecksum)
3939
@@ -50,9 +50,14 @@ func CloseAndBackupDB(a *App, memDb *sql.DB) error {
5050 return err
5151 }
5252
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+ }
5661 }
5762
5863 _, 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, err14+ 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 []string18+ if sqlTag != "" {
18- for rows.Next() {19+ tableNames = append(tableNames, sqlTag)
19- var tableName string20+ } else {
20- if err := rows.Scan(&tableName); err != nil {21+ // Use the field name as a fallback
21- return nil, err22+ 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 err32 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 err50 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 err55+ 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 (
99 "os"
1010 "path/filepath"
1111
12- "github.com/bnema/gordon/internal/db/migrate"
12+ pkgsqlite "github.com/bnema/gordon/pkg/sqlite"
1313 "github.com/mattn/go-sqlite3"
1414 )
1515
@@ -37,7 +37,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
3737 }
3838
3939 if !dbExists {
40- if err := bootstrapDB(dbPath); err != nil {
40+ if err := bootstrapDB(dbPath, a); err != nil {
4141 return nil, fmt.Errorf("failed to bootstrap DB: %w", err)
4242 }
4343 }
@@ -46,7 +46,7 @@ func InitializeDB(a *App) (*sql.DB, error) {
4646 return nil, fmt.Errorf("failed to load DB to memory: %w", err)
4747 }
4848 // Here we can generate the initial checksum
49- a.InitialChecksum, err = GenerateDBChecksum(memDb)
49+ a.InitialChecksum, err = GenerateDBChecksum(memDb, a)
5050 if err != nil {
5151 return nil, fmt.Errorf("failed to generate initial checksum: %w", err)
5252 }
@@ -76,25 +76,25 @@ func (a *App) checkDBFile(dbPath string) (bool, error) {
7676 return true, nil
7777 }
7878
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 {
8180 db, err := sql.Open("sqlite3", dbPath)
8281 if err != nil {
8382 return err
8483 }
8584 defer db.Close()
8685
87- if err := migrate.CreateUserTable(db); err != nil {
86+ tables := app.DBTables
87+
88+ if err := pkgsqlite.CreateTable(db, tables.User, "user"); err != nil {
8889 return err
8990 }
90- if err := migrate.CreateAccountTable(db); err != nil {
91+ if err := pkgsqlite.CreateTable(db, tables.Account, "account"); err != nil {
9192 return err
9293 }
93- if err := migrate.CreateSessionTable(db); err != nil {
94+ if err := pkgsqlite.CreateTable(db, tables.Sessions, "sessions"); err != nil {
9495 return err
9596 }
96-
97- if err := migrate.CreateProviderTable(db); err != nil {
97+ if err := pkgsqlite.CreateTable(db, tables.Provider, "provider"); err != nil {
9898 return err
9999 }
100100
@@ -165,12 +165,8 @@ func loadDBToMemory(dbPath string) (*sql.DB, error) {
165165 }
166166
167167 // 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)
174170 var finalChecksum []byte
175171
176172 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 checksum48 // 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, nil76 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 err82 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 err89 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 err92 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 err95 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 err98 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 []byte170 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 @@
11 package db
22
33 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"`
77 Account Account
88 }
99
1010 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
1414 Providers []Provider
1515 }
1616
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"`
2323 }
2424
2525 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"`
3232 }
@@ -1,32 +1,32 @@
1 package db1 package db
2 2
3 type User struct {3 type User struct {
4- ID int644+ ID int64 `sql:"id, primary_key"`
5- Name string5+ Name string `sql:"name"`
6- Email string6+ Email string `sql:"email"`
7 Account Account7 Account Account
8 }8 }
9 9
10 type Account struct {10 type Account struct {
11- ID int6411+ ID int64 `sql:"id, primary_key"`
12- UserID int6412+ UserID int64 `sql:"user_id, foreign_key=user.id"`
13- Sessions []Session13+ Sessions []Sessions
14 Providers []Provider14 Providers []Provider
15 }15 }
16 16
17-type Session struct {17+type Sessions struct {
18- ID int6418+ ID int64 `sql:"id, primary_key"`
19- AccountID int6419+ AccountID int64 `sql:"account_id, foreign_key=account.id"`
20- SessionToken string20+ BrowserInfo string `sql:"browser_info"`
21- Expires string21+ Expires string `sql:"expires"`
22- IsOnline bool22+ IsOnline bool `sql:"is_online"`
23 }23 }
24 24
25 type Provider struct {25 type Provider struct {
26- ID int6426+ ID int64 `sql:"id, primary_key"`
27- AccountID int6427+ AccountID int64 `sql:"account_id, foreign_key=account.id"`
28- Name string28+ Name string `sql:"name"`
29- AccessToken string29+ AccessToken string `sql:"access_token"`
30- RefreshToken string30+ RefreshToken string `sql:"refresh_token"`
31- Expires string31+ Expires string `sql:"expires"`
32 }32 }
M internal/httpserve/handler/oauth.go +34 -18
@@ -3,11 +3,11 @@ package handler
33 import (
44 "encoding/base64"
55 "fmt"
6- "log"
76 "net/http"
87 "os"
98
109 "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/db"
1111 "github.com/bnema/gordon/internal/db/queries"
1212 "github.com/bnema/gordon/internal/templates/render"
1313 _ "github.com/joho/godotenv/autoload"
@@ -28,7 +28,7 @@ func compareGordonToken(c echo.Context, a *app.App) error {
2828 // Check if there is already a user in the db which means that the admin is already setup
2929 func checkAdmin(a *app.App) (bool, error) {
3030 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)
3232 if err != nil {
3333 return false, err
3434 }
@@ -93,42 +93,58 @@ func StartOAuthGithub(c echo.Context, a *app.App) error {
9393 return c.Redirect(http.StatusFound, oauthURL)
9494 }
9595
96+type Sessions struct {
97+ *db.Sessions
98+}
99+
96100 func OAuthCallback(c echo.Context, a *app.App) error {
97101
98102 accessToken := c.QueryParam("access_token")
99103 encodedState := c.QueryParam("state")
100104
101- // Decode the state parameter to get the original redirectDomain
105+ // Decode the state parameter to validate the original redirectDomain
102106 _, err := base64.StdEncoding.DecodeString(encodedState)
103107 if err != nil {
104108 return c.String(http.StatusBadRequest, "Invalid state parameter")
105109 }
106110
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
121111 sess, err := session.Get("session", c)
122112 if err != nil {
123113 return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
124114 }
125-
126115 // Set the user as authenticated
127116 sess.Values["authenticated"] = true
128117 sess.Values["access_token"] = accessToken
118+ sess.Values["expires"] = 604800
129119 if err = sess.Save(c.Request(), c.Response()); err != nil {
130120 return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
131121 }
132122
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+ }
133149 return c.Redirect(http.StatusFound, "/admin")
134150 }
@@ -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 setup28 // 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 int30 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, err33 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 redirectDomain105+ // 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 authenticated115 // Set the user as authenticated
127 sess.Values["authenticated"] = true116 sess.Values["authenticated"] = true
128 sess.Values["access_token"] = accessToken117 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close