detached 10ffb9d

Refactor and Enhance Database and OAuth Handling

Update Go version in go.mod to 1.21.3
Introduce new DBTables struct to hold database tables
Add context package in sqliteinit.go for better DB connection management
Replace old and very wrong DB initialization and migration logic in sqliteinit.go
Update SQL query in tokeninit.go to test logic
Remove unused database migration and query files (bootstrapdb.go, inspectdb.go, userdb.go)
Update OAuth callback to fetch user information and enhance state parameter handling
Simplify logout logic in router.go
commit
10ffb9df7cfe8739c2a52386301f292693d37a6d
author
bnema · · 2023-10-13 06:59 UTC
parent
86ea5a2
signature
unsigned
12 files · +237 -492
M go.mod +5 -11
@@ -1,17 +1,17 @@
11 module github.com/bnema/gordon
22
3-go 1.21.1
3+go 1.21.3
44
55 require (
66 github.com/PuerkitoBio/goquery v1.8.1
77 github.com/docker/docker v24.0.6+incompatible
8- github.com/google/safehtml v0.1.0
8+ github.com/docker/go-connections v0.4.0
99 github.com/gorilla/sessions v1.2.1
1010 github.com/joho/godotenv v1.5.1
1111 github.com/labstack/echo-contrib v0.15.0
1212 github.com/labstack/echo/v4 v4.11.1
13- github.com/markbates/goth v1.78.0
1413 github.com/mattn/go-sqlite3 v1.14.17
14+ github.com/microcosm-cc/bluemonday v1.0.25
1515 gopkg.in/yaml.v3 v3.0.1
1616 )
1717
@@ -20,20 +20,16 @@ require (
2020 github.com/andybalholm/cascadia v1.3.1 // indirect
2121 github.com/aymerick/douceur v0.2.0 // indirect
2222 github.com/docker/distribution v2.8.2+incompatible // indirect
23- github.com/docker/go-connections v0.4.0 // indirect
2423 github.com/docker/go-units v0.5.0 // indirect
2524 github.com/gogo/protobuf v1.3.2 // indirect
2625 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
27- github.com/golang/protobuf v1.5.3 // indirect
28- github.com/google/go-cmp v0.5.9 // indirect
2926 github.com/gorilla/context v1.1.1 // indirect
3027 github.com/gorilla/css v1.0.0 // indirect
31- github.com/gorilla/mux v1.6.2 // indirect
3228 github.com/gorilla/securecookie v1.1.1 // indirect
29+ github.com/kr/text v0.1.0 // indirect
3330 github.com/labstack/gommon v0.4.0 // indirect
3431 github.com/mattn/go-colorable v0.1.13 // indirect
3532 github.com/mattn/go-isatty v0.0.19 // indirect
36- github.com/microcosm-cc/bluemonday v1.0.25 // indirect
3733 github.com/moby/term v0.5.0 // indirect
3834 github.com/morikuni/aec v1.0.0 // indirect
3935 github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -46,12 +42,10 @@ require (
4642 golang.org/x/crypto v0.11.0 // indirect
4743 golang.org/x/mod v0.9.0 // indirect
4844 golang.org/x/net v0.12.0 // indirect
49- golang.org/x/oauth2 v0.4.0 // indirect
5045 golang.org/x/sys v0.10.0 // indirect
5146 golang.org/x/text v0.11.0 // indirect
5247 golang.org/x/time v0.3.0 // indirect
5348 golang.org/x/tools v0.7.0 // indirect
54- google.golang.org/appengine v1.6.7 // indirect
55- google.golang.org/protobuf v1.29.1 // indirect
49+ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
5650 gotest.tools/v3 v3.5.0 // indirect
5751 )
@@ -1,17 +1,17 @@
1 module github.com/bnema/gordon1 module github.com/bnema/gordon
2 2
3-go 1.21.13+go 1.21.3
4 4
5 require (5 require (
6 github.com/PuerkitoBio/goquery v1.8.16 github.com/PuerkitoBio/goquery v1.8.1
7 github.com/docker/docker v24.0.6+incompatible7 github.com/docker/docker v24.0.6+incompatible
8- github.com/google/safehtml v0.1.08+ github.com/docker/go-connections v0.4.0
9 github.com/gorilla/sessions v1.2.19 github.com/gorilla/sessions v1.2.1
10 github.com/joho/godotenv v1.5.110 github.com/joho/godotenv v1.5.1
11 github.com/labstack/echo-contrib v0.15.011 github.com/labstack/echo-contrib v0.15.0
12 github.com/labstack/echo/v4 v4.11.112 github.com/labstack/echo/v4 v4.11.1
13- github.com/markbates/goth v1.78.0
14 github.com/mattn/go-sqlite3 v1.14.1713 github.com/mattn/go-sqlite3 v1.14.17
14+ github.com/microcosm-cc/bluemonday v1.0.25
15 gopkg.in/yaml.v3 v3.0.115 gopkg.in/yaml.v3 v3.0.1
16 )16 )
17 17
@@ -20,20 +20,16 @@ require (
20 github.com/andybalholm/cascadia v1.3.1 // indirect20 github.com/andybalholm/cascadia v1.3.1 // indirect
21 github.com/aymerick/douceur v0.2.0 // indirect21 github.com/aymerick/douceur v0.2.0 // indirect
22 github.com/docker/distribution v2.8.2+incompatible // indirect22 github.com/docker/distribution v2.8.2+incompatible // indirect
23- github.com/docker/go-connections v0.4.0 // indirect
24 github.com/docker/go-units v0.5.0 // indirect23 github.com/docker/go-units v0.5.0 // indirect
25 github.com/gogo/protobuf v1.3.2 // indirect24 github.com/gogo/protobuf v1.3.2 // indirect
26 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect25 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
27- github.com/golang/protobuf v1.5.3 // indirect
28- github.com/google/go-cmp v0.5.9 // indirect
29 github.com/gorilla/context v1.1.1 // indirect26 github.com/gorilla/context v1.1.1 // indirect
30 github.com/gorilla/css v1.0.0 // indirect27 github.com/gorilla/css v1.0.0 // indirect
31- github.com/gorilla/mux v1.6.2 // indirect
32 github.com/gorilla/securecookie v1.1.1 // indirect28 github.com/gorilla/securecookie v1.1.1 // indirect
29+ github.com/kr/text v0.1.0 // indirect
33 github.com/labstack/gommon v0.4.0 // indirect30 github.com/labstack/gommon v0.4.0 // indirect
34 github.com/mattn/go-colorable v0.1.13 // indirect31 github.com/mattn/go-colorable v0.1.13 // indirect
35 github.com/mattn/go-isatty v0.0.19 // indirect32 github.com/mattn/go-isatty v0.0.19 // indirect
36- github.com/microcosm-cc/bluemonday v1.0.25 // indirect
37 github.com/moby/term v0.5.0 // indirect33 github.com/moby/term v0.5.0 // indirect
38 github.com/morikuni/aec v1.0.0 // indirect34 github.com/morikuni/aec v1.0.0 // indirect
39 github.com/opencontainers/go-digest v1.0.0 // indirect35 github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -46,12 +42,10 @@ require (
46 golang.org/x/crypto v0.11.0 // indirect42 golang.org/x/crypto v0.11.0 // indirect
47 golang.org/x/mod v0.9.0 // indirect43 golang.org/x/mod v0.9.0 // indirect
48 golang.org/x/net v0.12.0 // indirect44 golang.org/x/net v0.12.0 // indirect
49- golang.org/x/oauth2 v0.4.0 // indirect
50 golang.org/x/sys v0.10.0 // indirect45 golang.org/x/sys v0.10.0 // indirect
51 golang.org/x/text v0.11.0 // indirect46 golang.org/x/text v0.11.0 // indirect
52 golang.org/x/time v0.3.0 // indirect47 golang.org/x/time v0.3.0 // indirect
53 golang.org/x/tools v0.7.0 // indirect48 golang.org/x/tools v0.7.0 // indirect
54- google.golang.org/appengine v1.6.7 // indirect49+ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
55- google.golang.org/protobuf v1.29.1 // indirect
56 gotest.tools/v3 v3.5.0 // indirect50 gotest.tools/v3 v3.5.0 // indirect
57 )51 )
M go.sum +0 -375 generated file: collapsed
M internal/app/app.go +10 -1
@@ -8,6 +8,7 @@ import (
88 "log"
99 "os"
1010
11+ "github.com/bnema/gordon/internal/db"
1112 "github.com/bnema/gordon/internal/templates"
1213 "github.com/bnema/gordon/internal/webui"
1314 "github.com/bnema/gordon/pkg/docker"
@@ -27,8 +28,9 @@ type App struct {
2728 DBFilename string
2829 DBPath string
2930 InitialChecksum string
30- DB *sql.DB
3131 Config Config
32+ DB *sql.DB
33+ DBTables DBTables
3234 }
3335 type Config struct {
3436 General GeneralConfig `yaml:"General"`
@@ -62,6 +64,13 @@ type ContainerEngineConfig struct {
6264 Network string `yaml:"network"`
6365 }
6466
67+type DBTables struct {
68+ User db.User
69+ Account db.Account
70+ Session db.Session
71+ Provider db.Provider
72+}
73+
6574 func LoadConfig(config *Config) (*Config, error) {
6675 // Load env elements
6776 config.General.BuildVersion = os.Getenv("BUILD_VERSION")
@@ -8,6 +8,7 @@ import (
8 "log"8 "log"
9 "os"9 "os"
10 10
11+ "github.com/bnema/gordon/internal/db"
11 "github.com/bnema/gordon/internal/templates"12 "github.com/bnema/gordon/internal/templates"
12 "github.com/bnema/gordon/internal/webui"13 "github.com/bnema/gordon/internal/webui"
13 "github.com/bnema/gordon/pkg/docker"14 "github.com/bnema/gordon/pkg/docker"
@@ -27,8 +28,9 @@ type App struct {
27 DBFilename string28 DBFilename string
28 DBPath string29 DBPath string
29 InitialChecksum string30 InitialChecksum string
30- DB *sql.DB
31 Config Config31 Config Config
32+ DB *sql.DB
33+ DBTables DBTables
32 }34 }
33 type Config struct {35 type Config struct {
34 General GeneralConfig `yaml:"General"`36 General GeneralConfig `yaml:"General"`
@@ -62,6 +64,13 @@ type ContainerEngineConfig struct {
62 Network string `yaml:"network"`64 Network string `yaml:"network"`
63 }65 }
64 66
67+type DBTables struct {
68+ User db.User
69+ Account db.Account
70+ Session db.Session
71+ Provider db.Provider
72+}
73+
65 func LoadConfig(config *Config) (*Config, error) {74 func LoadConfig(config *Config) (*Config, error) {
66 // Load env elements75 // Load env elements
67 config.General.BuildVersion = os.Getenv("BUILD_VERSION")76 config.General.BuildVersion = os.Getenv("BUILD_VERSION")
M internal/app/sqliteinit.go +51 -5
@@ -1,6 +1,7 @@
11 package app
22
33 import (
4+ "context"
45 "crypto/md5"
56 "database/sql"
67 "encoding/hex"
@@ -8,8 +9,8 @@ import (
89 "os"
910 "path/filepath"
1011
11- "github.com/bnema/gordon/internal/database/migrate"
12- _ "github.com/mattn/go-sqlite3"
12+ "github.com/bnema/gordon/internal/db/migrate"
13+ "github.com/mattn/go-sqlite3"
1314 )
1415
1516 const (
@@ -49,6 +50,9 @@ func InitializeDB(a *App) (*sql.DB, error) {
4950 if err != nil {
5051 return nil, fmt.Errorf("failed to generate initial checksum: %w", err)
5152 }
53+ // Initialize the DB field in the App struct
54+ a.DB = memDb
55+
5256 return memDb, nil
5357 }
5458
@@ -90,6 +94,10 @@ func bootstrapDB(dbPath string) error {
9094 return err
9195 }
9296
97+ if err := migrate.CreateProviderTable(db); err != nil {
98+ return err
99+ }
100+
93101 return nil
94102 }
95103
@@ -100,17 +108,55 @@ func loadDBToMemory(dbPath string) (*sql.DB, error) {
100108 return nil, err
101109 }
102110
103- _, err = memDb.Exec("ATTACH DATABASE '" + dbPath + "' AS diskdb")
111+ diskDb, err := sql.Open("sqlite3", dbPath)
112+ if err != nil {
113+ return nil, err
114+ }
115+ defer diskDb.Close()
116+
117+ // Get a single connection from each *sql.DB
118+ memConn, err := memDb.Conn(context.Background())
104119 if err != nil {
105120 return nil, err
106121 }
122+ defer memConn.Close()
107123
108- _, err = memDb.Exec("CREATE TABLE users AS SELECT * FROM diskdb.users")
124+ diskConn, err := diskDb.Conn(context.Background())
109125 if err != nil {
110126 return nil, err
111127 }
128+ defer diskConn.Close()
129+
130+ err = memConn.Raw(func(driverConn interface{}) error {
131+ memSqliteConn, ok := driverConn.(*sqlite3.SQLiteConn)
132+ if !ok {
133+ return fmt.Errorf("failed to cast to *sqlite3.SQLiteConn")
134+ }
135+
136+ return diskConn.Raw(func(driverConn interface{}) error {
137+ diskSqliteConn, ok := driverConn.(*sqlite3.SQLiteConn)
138+ if !ok {
139+ return fmt.Errorf("failed to cast to *sqlite3.SQLiteConn")
140+ }
141+
142+ backup, err := memSqliteConn.Backup("main", diskSqliteConn, "main")
143+ if err != nil {
144+ return err
145+ }
146+ defer backup.Close()
147+
148+ done, err := backup.Step(-1)
149+ if err != nil {
150+ return err
151+ }
152+ if !done {
153+ return fmt.Errorf("backup not fully completed")
154+ }
155+
156+ return backup.Finish()
157+ })
158+ })
112159
113- _, err = memDb.Exec("DETACH DATABASE diskdb")
114160 if err != nil {
115161 return nil, err
116162 }
@@ -1,6 +1,7 @@
1 package app1 package app
2 2
3 import (3 import (
4+ "context"
4 "crypto/md5"5 "crypto/md5"
5 "database/sql"6 "database/sql"
6 "encoding/hex"7 "encoding/hex"
@@ -8,8 +9,8 @@ import (
8 "os"9 "os"
9 "path/filepath"10 "path/filepath"
10 11
11- "github.com/bnema/gordon/internal/database/migrate"12+ "github.com/bnema/gordon/internal/db/migrate"
12- _ "github.com/mattn/go-sqlite3"13+ "github.com/mattn/go-sqlite3"
13 )14 )
14 15
15 const (16 const (
@@ -49,6 +50,9 @@ func InitializeDB(a *App) (*sql.DB, error) {
49 if err != nil {50 if err != nil {
50 return nil, fmt.Errorf("failed to generate initial checksum: %w", err)51 return nil, fmt.Errorf("failed to generate initial checksum: %w", err)
51 }52 }
53+ // Initialize the DB field in the App struct
54+ a.DB = memDb
55+
52 return memDb, nil56 return memDb, nil
53 }57 }
54 58
@@ -90,6 +94,10 @@ func bootstrapDB(dbPath string) error {
90 return err94 return err
91 }95 }
92 96
97+ if err := migrate.CreateProviderTable(db); err != nil {
98+ return err
99+ }
100+
93 return nil101 return nil
94 }102 }
95 103
@@ -100,17 +108,55 @@ func loadDBToMemory(dbPath string) (*sql.DB, error) {
100 return nil, err108 return nil, err
101 }109 }
102 110
103- _, err = memDb.Exec("ATTACH DATABASE '" + dbPath + "' AS diskdb")111+ diskDb, err := sql.Open("sqlite3", dbPath)
112+ if err != nil {
113+ return nil, err
114+ }
115+ defer diskDb.Close()
116+
117+ // Get a single connection from each *sql.DB
118+ memConn, err := memDb.Conn(context.Background())
104 if err != nil {119 if err != nil {
105 return nil, err120 return nil, err
106 }121 }
122+ defer memConn.Close()
107 123
108- _, err = memDb.Exec("CREATE TABLE users AS SELECT * FROM diskdb.users")124+ diskConn, err := diskDb.Conn(context.Background())
109 if err != nil {125 if err != nil {
110 return nil, err126 return nil, err
111 }127 }
128+ defer diskConn.Close()
129+
130+ err = memConn.Raw(func(driverConn interface{}) error {
131+ memSqliteConn, ok := driverConn.(*sqlite3.SQLiteConn)
132+ if !ok {
133+ return fmt.Errorf("failed to cast to *sqlite3.SQLiteConn")
134+ }
135+
136+ return diskConn.Raw(func(driverConn interface{}) error {
137+ diskSqliteConn, ok := driverConn.(*sqlite3.SQLiteConn)
138+ if !ok {
139+ return fmt.Errorf("failed to cast to *sqlite3.SQLiteConn")
140+ }
141+
142+ backup, err := memSqliteConn.Backup("main", diskSqliteConn, "main")
143+ if err != nil {
144+ return err
145+ }
146+ defer backup.Close()
147+
148+ done, err := backup.Step(-1)
149+ if err != nil {
150+ return err
151+ }
152+ if !done {
153+ return fmt.Errorf("backup not fully completed")
154+ }
155+
156+ return backup.Finish()
157+ })
158+ })
112 159
113- _, err = memDb.Exec("DETACH DATABASE diskdb")
114 if err != nil {160 if err != nil {
115 return nil, err161 return nil, err
116 }162 }
M internal/app/tokeninit.go +4 -2
@@ -9,7 +9,7 @@ import (
99 // HandleNewInitialization checks if an admin user exists in db, and if not, generates a token for the initial login and stores it in the config file
1010 func (a *App) HandleNewTokenInitialization() (string, error) {
1111 // Query to check if a user with id=1 exists
12- query := `SELECT COUNT(id) FROM users WHERE id = "1"`
12+ query := `SELECT COUNT(id) FROM user WHERE id = "1"`
1313 var count int
1414 err := a.DB.QueryRow(query).Scan(&count)
1515 if err != nil {
@@ -27,7 +27,9 @@ func (a *App) HandleNewTokenInitialization() (string, error) {
2727 return "", fmt.Errorf("failed to generate token: %v", err)
2828 }
2929
30- fmt.Printf("Initial login token: %s\n", token)
30+ // url = config.Http.SubDomain + config.Http.TopDomain + config.Http.Port + config.Admin.Path
31+
32+ fmt.Printf("Login with the new token: %s\n", token)
3133
3234 // Store the token in the config file
3335 a.Config.General.GordonToken = token
@@ -9,7 +9,7 @@ import (
9 // HandleNewInitialization checks if an admin user exists in db, and if not, generates a token for the initial login and stores it in the config file9 // HandleNewInitialization checks if an admin user exists in db, and if not, generates a token for the initial login and stores it in the config file
10 func (a *App) HandleNewTokenInitialization() (string, error) {10 func (a *App) HandleNewTokenInitialization() (string, error) {
11 // Query to check if a user with id=1 exists11 // Query to check if a user with id=1 exists
12- query := `SELECT COUNT(id) FROM users WHERE id = "1"`12+ query := `SELECT COUNT(id) FROM user WHERE id = "1"`
13 var count int13 var count int
14 err := a.DB.QueryRow(query).Scan(&count)14 err := a.DB.QueryRow(query).Scan(&count)
15 if err != nil {15 if err != nil {
@@ -27,7 +27,9 @@ func (a *App) HandleNewTokenInitialization() (string, error) {
27 return "", fmt.Errorf("failed to generate token: %v", err)27 return "", fmt.Errorf("failed to generate token: %v", err)
28 }28 }
29 29
30- fmt.Printf("Initial login token: %s\n", token)30+ // url = config.Http.SubDomain + config.Http.TopDomain + config.Http.Port + config.Admin.Path
31+
32+ fmt.Printf("Login with the new token: %s\n", token)
31 33
32 // Store the token in the config file34 // Store the token in the config file
33 a.Config.General.GordonToken = token35 a.Config.General.GordonToken = token
D internal/database/migrate/bootstrapdb.go +0 -70
@@ -1,70 +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 users (
12- id VARCHAR(255) PRIMARY KEY,
13- name VARCHAR(255) UNIQUE NOT NULL,
14- email VARCHAR(255) UNIQUE NOT NULL
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 accounts (
29- id VARCHAR(255) PRIMARY KEY,
30- user_id VARCHAR(255) NOT NULL,
31- type VARCHAR(50) NOT NULL,
32- provider VARCHAR(50) NOT NULL,
33- provider_account_id VARCHAR(255) NOT NULL,
34- refresh_token VARCHAR(255),
35- access_token VARCHAR(255),
36- expires_at DATETIME,
37- token_type VARCHAR(50),
38- scope VARCHAR(255),
39- id_token VARCHAR(255),
40- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
41- );`
42-
43- _, err := db.Exec(createTableSQL)
44- if err != nil {
45- return fmt.Errorf("failed to create account table: %v", err)
46- }
47-
48- return nil
49-}
50-
51-// CreateSessionTable creates the 'sessions' table.
52-func CreateSessionTable(db *sql.DB) error {
53- createTableSQL := `
54- CREATE TABLE sessions (
55- id VARCHAR(255) PRIMARY KEY,
56- session_token VARCHAR(255) UNIQUE NOT NULL,
57- user_id VARCHAR(255) NOT NULL,
58- expires DATETIME,
59- is_online BOOLEAN DEFAULT FALSE,
60- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
61- );
62- CREATE INDEX idx_sessions_user_id ON sessions(user_id);`
63-
64- _, err := db.Exec(createTableSQL)
65- if err != nil {
66- return fmt.Errorf("failed to create session table: %v", err)
67- }
68-
69- return nil
70-}
@@ -1,70 +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 users (
12- id VARCHAR(255) PRIMARY KEY,
13- name VARCHAR(255) UNIQUE NOT NULL,
14- email VARCHAR(255) UNIQUE NOT NULL
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 accounts (
29- id VARCHAR(255) PRIMARY KEY,
30- user_id VARCHAR(255) NOT NULL,
31- type VARCHAR(50) NOT NULL,
32- provider VARCHAR(50) NOT NULL,
33- provider_account_id VARCHAR(255) NOT NULL,
34- refresh_token VARCHAR(255),
35- access_token VARCHAR(255),
36- expires_at DATETIME,
37- token_type VARCHAR(50),
38- scope VARCHAR(255),
39- id_token VARCHAR(255),
40- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
41- );`
42-
43- _, err := db.Exec(createTableSQL)
44- if err != nil {
45- return fmt.Errorf("failed to create account table: %v", err)
46- }
47-
48- return nil
49-}
50-
51-// CreateSessionTable creates the 'sessions' table.
52-func CreateSessionTable(db *sql.DB) error {
53- createTableSQL := `
54- CREATE TABLE sessions (
55- id VARCHAR(255) PRIMARY KEY,
56- session_token VARCHAR(255) UNIQUE NOT NULL,
57- user_id VARCHAR(255) NOT NULL,
58- expires DATETIME,
59- is_online BOOLEAN DEFAULT FALSE,
60- FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
61- );
62- CREATE INDEX idx_sessions_user_id ON sessions(user_id);`
63-
64- _, err := db.Exec(createTableSQL)
65- if err != nil {
66- return fmt.Errorf("failed to create session table: %v", err)
67- }
68-
69- return nil
70-}
A internal/db/migrate/bootstrapdb.go +81 -0
@@ -0,0 +1,81 @@
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+}
@@ -0,0 +1,81 @@
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+}
R internal/database/queries/inspectdb.go → internal/db/queries/inspectdb.go +25 -6 renamed, 58% similar
@@ -1,12 +1,14 @@
11 package queries
22
33 import (
4- "database/sql"
54 "fmt"
5+
6+ "github.com/bnema/gordon/internal/app"
67 )
78
89 // InspectInMemoryDB inspects the in-memory database. (for debug purpose)
9-func InspectInMemoryDB(memDb *sql.DB) error {
10+func InspectInMemoryDB(a *app.App) error {
11+ memDb := a.DB
1012 // Query the sqlite_master table to get a list of all tables
1113 rows, err := memDb.Query("SELECT name FROM sqlite_master WHERE type='table'")
1214 fmt.Println("rows", rows)
@@ -33,15 +35,14 @@ func InspectInMemoryDB(memDb *sql.DB) error {
3335 for _, table := range tables {
3436 fmt.Println(table)
3537 }
36-
37- // If you want to see the content of a specific table (e.g., users):
38- userRows, err := memDb.Query("SELECT * FROM users")
38+ // Print the content of the users table
39+ userRows, err := memDb.Query("SELECT * FROM user")
3940 if err != nil {
4041 return err
4142 }
4243 defer userRows.Close()
4344
44- fmt.Println("\nContent of the 'users' table:")
45+ fmt.Println("\nContent of the 'user' table:")
4546 for userRows.Next() {
4647 // Assuming users table has columns: id, name
4748 var id int
@@ -52,5 +53,23 @@ func InspectInMemoryDB(memDb *sql.DB) error {
5253 fmt.Printf("ID: %d, Name: %s\n", id, name)
5354 }
5455
56+ // Print the content of accounts table
57+ accountRows, err := memDb.Query("SELECT * FROM account")
58+ if err != nil {
59+ return err
60+ }
61+
62+ fmt.Println("\nContent of the 'account' table:")
63+ for accountRows.Next() {
64+ // Assuming accounts table has columns: id, name, user_id
65+ var id int
66+ var name string
67+ var userID int
68+ if err := accountRows.Scan(&id, &name, &userID); err != nil {
69+ return err
70+ }
71+ fmt.Printf("ID: %d, Name: %s, UserID: %d\n", id, name, userID)
72+ }
73+
5574 return nil
5675 }
@@ -1,12 +1,14 @@
1 package queries1 package queries
2 2
3 import (3 import (
4- "database/sql"
5 "fmt"4 "fmt"
5+
6+ "github.com/bnema/gordon/internal/app"
6 )7 )
7 8
8 // InspectInMemoryDB inspects the in-memory database. (for debug purpose)9 // InspectInMemoryDB inspects the in-memory database. (for debug purpose)
9-func InspectInMemoryDB(memDb *sql.DB) error {10+func InspectInMemoryDB(a *app.App) error {
11+ memDb := a.DB
10 // Query the sqlite_master table to get a list of all tables12 // Query the sqlite_master table to get a list of all tables
11 rows, err := memDb.Query("SELECT name FROM sqlite_master WHERE type='table'")13 rows, err := memDb.Query("SELECT name FROM sqlite_master WHERE type='table'")
12 fmt.Println("rows", rows)14 fmt.Println("rows", rows)
@@ -33,15 +35,14 @@ func InspectInMemoryDB(memDb *sql.DB) error {
33 for _, table := range tables {35 for _, table := range tables {
34 fmt.Println(table)36 fmt.Println(table)
35 }37 }
36-38+ // Print the content of the users table
37- // If you want to see the content of a specific table (e.g., users):39+ userRows, err := memDb.Query("SELECT * FROM user")
38- userRows, err := memDb.Query("SELECT * FROM users")
39 if err != nil {40 if err != nil {
40 return err41 return err
41 }42 }
42 defer userRows.Close()43 defer userRows.Close()
43 44
44- fmt.Println("\nContent of the 'users' table:")45+ fmt.Println("\nContent of the 'user' table:")
45 for userRows.Next() {46 for userRows.Next() {
46 // Assuming users table has columns: id, name47 // Assuming users table has columns: id, name
47 var id int48 var id int
@@ -52,5 +53,23 @@ func InspectInMemoryDB(memDb *sql.DB) error {
52 fmt.Printf("ID: %d, Name: %s\n", id, name)53 fmt.Printf("ID: %d, Name: %s\n", id, name)
53 }54 }
54 55
56+ // Print the content of accounts table
57+ accountRows, err := memDb.Query("SELECT * FROM account")
58+ if err != nil {
59+ return err
60+ }
61+
62+ fmt.Println("\nContent of the 'account' table:")
63+ for accountRows.Next() {
64+ // Assuming accounts table has columns: id, name, user_id
65+ var id int
66+ var name string
67+ var userID int
68+ if err := accountRows.Scan(&id, &name, &userID); err != nil {
69+ return err
70+ }
71+ fmt.Printf("ID: %d, Name: %s, UserID: %d\n", id, name, userID)
72+ }
73+
55 return nil74 return nil
56 }75 }
R internal/database/queries/userdb.go → internal/db/queries/userdb.go +13 -3 renamed, 72% similar
@@ -1,9 +1,8 @@
11 package queries
22
33 import (
4- "database/sql"
5-
64 "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
76 )
87
98 // Pseudo-code for all queries that will be needed on the user database
@@ -13,7 +12,8 @@ import (
1312 // 1. Create a user (one only, if id=1 then it's the admin)
1413 // This happens when the user logs in for the first time
1514 // TODO: prevent anyone from creating another user
16-func CreateUser(a *app.App, db *sql.DB) error {
15+func CreateUser(a *app.App) error {
16+
1717 // To create a user i need to :
1818 // 1. check if gordontoken == gordontoken inside the yaml config file
1919 // 2. check if user already exists
@@ -24,3 +24,13 @@ func CreateUser(a *app.App, db *sql.DB) error {
2424 // 7. return the user informations (id, name, email, image, is_admin)
2525 return nil
2626 }
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,9 +1,8 @@
1 package queries1 package queries
2 2
3 import (3 import (
4- "database/sql"
5-
6 "github.com/bnema/gordon/internal/app"4 "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
7 )6 )
8 7
9 // Pseudo-code for all queries that will be needed on the user database8 // Pseudo-code for all queries that will be needed on the user database
@@ -13,7 +12,8 @@ import (
13 // 1. Create a user (one only, if id=1 then it's the admin)12 // 1. Create a user (one only, if id=1 then it's the admin)
14 // This happens when the user logs in for the first time13 // This happens when the user logs in for the first time
15 // TODO: prevent anyone from creating another user14 // TODO: prevent anyone from creating another user
16-func CreateUser(a *app.App, db *sql.DB) error {15+func CreateUser(a *app.App) error {
16+
17 // To create a user i need to :17 // To create a user i need to :
18 // 1. check if gordontoken == gordontoken inside the yaml config file18 // 1. check if gordontoken == gordontoken inside the yaml config file
19 // 2. check if user already exists19 // 2. check if user already exists
@@ -24,3 +24,13 @@ func CreateUser(a *app.App, db *sql.DB) error {
24 // 7. return the user informations (id, name, email, image, is_admin)24 // 7. return the user informations (id, name, email, image, is_admin)
25 return nil25 return nil
26 }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/struct.go +32 -0
@@ -0,0 +1,32 @@
1+package db
2+
3+type User struct {
4+ ID int64
5+ Name string
6+ Email string
7+ Account Account
8+}
9+
10+type Account struct {
11+ ID int64
12+ UserID int64
13+ Sessions []Session
14+ Providers []Provider
15+}
16+
17+type Session struct {
18+ ID int64
19+ AccountID int64
20+ SessionToken string
21+ Expires string
22+ IsOnline bool
23+}
24+
25+type Provider struct {
26+ ID int64
27+ AccountID int64
28+ Name string
29+ AccessToken string
30+ RefreshToken string
31+ Expires string
32+}
@@ -0,0 +1,32 @@
1+package db
2+
3+type User struct {
4+ ID int64
5+ Name string
6+ Email string
7+ Account Account
8+}
9+
10+type Account struct {
11+ ID int64
12+ UserID int64
13+ Sessions []Session
14+ Providers []Provider
15+}
16+
17+type Session struct {
18+ ID int64
19+ AccountID int64
20+ SessionToken string
21+ Expires string
22+ IsOnline bool
23+}
24+
25+type Provider struct {
26+ ID int64
27+ AccountID int64
28+ Name string
29+ AccessToken string
30+ RefreshToken string
31+ Expires string
32+}
M internal/httpserve/handler/oauth.go +15 -18
@@ -3,16 +3,16 @@ package handler
33 import (
44 "encoding/base64"
55 "fmt"
6+ "log"
67 "net/http"
78 "os"
8- "strings"
99
1010 "github.com/bnema/gordon/internal/app"
11+ "github.com/bnema/gordon/internal/db/queries"
1112 "github.com/bnema/gordon/internal/templates/render"
1213 _ "github.com/joho/godotenv/autoload"
1314 "github.com/labstack/echo-contrib/session"
1415 "github.com/labstack/echo/v4"
15- "github.com/markbates/goth/gothic"
1616 )
1717
1818 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml
@@ -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 users").Scan(&count)
31+ err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count)
3232 if err != nil {
3333 return false, err
3434 }
@@ -94,25 +94,28 @@ func StartOAuthGithub(c echo.Context, a *app.App) error {
9494 }
9595
9696 func OAuthCallback(c echo.Context, a *app.App) error {
97+
9798 accessToken := c.QueryParam("access_token")
9899 encodedState := c.QueryParam("state")
99100
100101 // Decode the state parameter to get the original redirectDomain
101- decodedState, err := base64.StdEncoding.DecodeString(encodedState)
102+ _, err := base64.StdEncoding.DecodeString(encodedState)
102103 if err != nil {
103104 return c.String(http.StatusBadRequest, "Invalid state parameter")
104105 }
105106
106- state := string(decodedState)
107- parts := strings.SplitN(state, ":", 2)
108- if len(parts) != 2 || parts[0] != "redirectDomain" {
109- return c.String(http.StatusBadRequest, "Invalid state format")
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")
110112 }
111113
112- // Retrieve the original redirectDomain
113- redirectDomain := parts[1]
114- fmt.Print(redirectDomain)
115- // Now, you can use accessToken for whatever you need in your application
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
116119
117120 // Also, you can save the authentication state in the session
118121 sess, err := session.Get("session", c)
@@ -129,9 +132,3 @@ func OAuthCallback(c echo.Context, a *app.App) error {
129132
130133 return c.Redirect(http.StatusFound, "/admin")
131134 }
132-
133-// Logout handles user logout and clears session
134-func Logout(c echo.Context, a *app.App) error {
135- gothic.Logout(c.Response(), c.Request())
136- return c.Redirect(http.StatusMovedPermanently, "/")
137-}
@@ -3,16 +3,16 @@ package handler
3 import (3 import (
4 "encoding/base64"4 "encoding/base64"
5 "fmt"5 "fmt"
6+ "log"
6 "net/http"7 "net/http"
7 "os"8 "os"
8- "strings"
9 9
10 "github.com/bnema/gordon/internal/app"10 "github.com/bnema/gordon/internal/app"
11+ "github.com/bnema/gordon/internal/db/queries"
11 "github.com/bnema/gordon/internal/templates/render"12 "github.com/bnema/gordon/internal/templates/render"
12 _ "github.com/joho/godotenv/autoload"13 _ "github.com/joho/godotenv/autoload"
13 "github.com/labstack/echo-contrib/session"14 "github.com/labstack/echo-contrib/session"
14 "github.com/labstack/echo/v4"15 "github.com/labstack/echo/v4"
15- "github.com/markbates/goth/gothic"
16 )16 )
17 17
18 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml18 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml
@@ -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 users").Scan(&count)31+ err := a.DB.QueryRow("SELECT COUNT(*) FROM user").Scan(&count)
32 if err != nil {32 if err != nil {
33 return false, err33 return false, err
34 }34 }
@@ -94,25 +94,28 @@ func StartOAuthGithub(c echo.Context, a *app.App) error {
94 }94 }
95 95
96 func OAuthCallback(c echo.Context, a *app.App) error {96 func OAuthCallback(c echo.Context, a *app.App) error {
97+
97 accessToken := c.QueryParam("access_token")98 accessToken := c.QueryParam("access_token")
98 encodedState := c.QueryParam("state")99 encodedState := c.QueryParam("state")
99 100
100 // Decode the state parameter to get the original redirectDomain101 // Decode the state parameter to get the original redirectDomain
101- decodedState, err := base64.StdEncoding.DecodeString(encodedState)102+ _, err := base64.StdEncoding.DecodeString(encodedState)
102 if err != nil {103 if err != nil {
103 return c.String(http.StatusBadRequest, "Invalid state parameter")104 return c.String(http.StatusBadRequest, "Invalid state parameter")
104 }105 }
105 106
106- state := string(decodedState)107+ // Get the user information from the OAuth provider
107- parts := strings.SplitN(state, ":", 2)108+ userInfo, err := queries.GetUserInformations(a, accessToken)
108- if len(parts) != 2 || parts[0] != "redirectDomain" {109+ if err != nil {
109- return c.String(http.StatusBadRequest, "Invalid state format")110+ log.Printf("Failed to get user information: %v\n", err)
111+ return echo.NewHTTPError(http.StatusInternalServerError, "Failed to get user information")
110 }112 }
111 113
112- // Retrieve the original redirectDomain114+ fmt.Printf("User info: %+v\n", userInfo)
113- redirectDomain := parts[1]115+
114- fmt.Print(redirectDomain)116+ // If the user does not exist, create it
115- // Now, you can use accessToken for whatever you need in your application117+
118+ // If the user exists, update the access_token for this provider
116 119
117 // Also, you can save the authentication state in the session120 // Also, you can save the authentication state in the session
118 sess, err := session.Get("session", c)121 sess, err := session.Get("session", c)
@@ -129,9 +132,3 @@ func OAuthCallback(c echo.Context, a *app.App) error {
129 132
130 return c.Redirect(http.StatusFound, "/admin")133 return c.Redirect(http.StatusFound, "/admin")
131 }134 }
132-
133-// Logout handles user logout and clears session
134-func Logout(c echo.Context, a *app.App) error {
135- gothic.Logout(c.Response(), c.Request())
136- return c.Redirect(http.StatusMovedPermanently, "/")
137-}
M internal/httpserve/router.go +1 -1
@@ -68,7 +68,7 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
6868 return handler.OAuthCallback(c, a)
6969 })
7070 e.GET(adminPath+"/logout", func(c echo.Context) error {
71- return handler.Logout(c, a)
71+ return c.HTML(200, "<h1>Logout</h1>")
7272 })
7373 }
7474
@@ -68,7 +68,7 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
68 return handler.OAuthCallback(c, a)68 return handler.OAuthCallback(c, a)
69 })69 })
70 e.GET(adminPath+"/logout", func(c echo.Context) error {70 e.GET(adminPath+"/logout", func(c echo.Context) error {
71- return handler.Logout(c, a)71+ return c.HTML(200, "<h1>Logout</h1>")
72 })72 })
73 }73 }
74 74

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