detached 5dc500e

Initializing token handling, enhancing app config, streamlining user table schema, and add token check for login before OAuth.

commit
5dc500e9e4f2b6618db7ef85ef1260a271a8de4e
author
bnema · · 2023-10-12 14:16 UTC
parent
717e73e
signature
unsigned
6 files · +134 -6
M cmd/srv/main.go +2 -0
@@ -31,6 +31,8 @@ func main() {
3131 // Pass memDb to the app with the rest of the configs
3232 a.DB = memDb
3333
34+ a.HandleNewTokenInitialization()
35+
3436 dockerClient := &docker.DockerClient{}
3537 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
3638 if err != nil {
@@ -31,6 +31,8 @@ func main() {
31 // Pass memDb to the app with the rest of the configs31 // Pass memDb to the app with the rest of the configs
32 a.DB = memDb32 a.DB = memDb
33 33
34+ a.HandleNewTokenInitialization()
35+
34 dockerClient := &docker.DockerClient{}36 dockerClient := &docker.DockerClient{}
35 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())37 err = dockerClient.InitializeClient(a.Config.NewDockerConfig())
36 if err != nil {38 if err != nil {
M internal/app/app.go +20 -1
@@ -41,7 +41,7 @@ type GeneralConfig struct {
4141 RunEnv string // come from env
4242 BuildDir string // come from env
4343 BuildVersion string // come from env
44- StorageDir string `yaml:"storageDir"`
44+ StorageDir string `yaml:"storageDir"` // = buildir/storage
4545 GordonToken string `yaml:"gordonToken"`
4646 }
4747
@@ -95,6 +95,8 @@ func NewApp() *App {
9595 if err != nil {
9696 log.Fatalf("Failed to load configuration: %v", err)
9797 }
98+ // Append build dir to storage dir
99+ config.General.StorageDir = fmt.Sprintf("%s/%s", config.General.BuildDir, config.General.StorageDir)
98100
99101 // Open the strings.yml file containing the strings for the current language
100102 file, err := templates.TemplateFS.Open("locstrings.yml")
@@ -157,3 +159,20 @@ func (config *Config) GenerateOauthCallbackURL() string {
157159
158160 return fmt.Sprintf("%s://%s%s%s/login/oauth/callback", scheme, domain, port, config.Admin.Path)
159161 }
162+
163+// UpdateConfig updates the config file
164+func (config *Config) UpdateConfig() error {
165+ // Marshal the config struct into YAML
166+ data, err := yaml.Marshal(config)
167+ if err != nil {
168+ return fmt.Errorf("failed to marshal config: %v", err)
169+ }
170+
171+ // Write the data to the config file (located in the build directory)
172+ err = os.WriteFile(fmt.Sprintf("%s/config.yml", config.General.BuildDir), data, 0644)
173+ if err != nil {
174+ return fmt.Errorf("failed to write to config file: %v", err)
175+ }
176+
177+ return nil
178+}
@@ -41,7 +41,7 @@ type GeneralConfig struct {
41 RunEnv string // come from env41 RunEnv string // come from env
42 BuildDir string // come from env42 BuildDir string // come from env
43 BuildVersion string // come from env43 BuildVersion string // come from env
44- StorageDir string `yaml:"storageDir"`44+ StorageDir string `yaml:"storageDir"` // = buildir/storage
45 GordonToken string `yaml:"gordonToken"`45 GordonToken string `yaml:"gordonToken"`
46 }46 }
47 47
@@ -95,6 +95,8 @@ func NewApp() *App {
95 if err != nil {95 if err != nil {
96 log.Fatalf("Failed to load configuration: %v", err)96 log.Fatalf("Failed to load configuration: %v", err)
97 }97 }
98+ // Append build dir to storage dir
99+ config.General.StorageDir = fmt.Sprintf("%s/%s", config.General.BuildDir, config.General.StorageDir)
98 100
99 // Open the strings.yml file containing the strings for the current language101 // Open the strings.yml file containing the strings for the current language
100 file, err := templates.TemplateFS.Open("locstrings.yml")102 file, err := templates.TemplateFS.Open("locstrings.yml")
@@ -157,3 +159,20 @@ func (config *Config) GenerateOauthCallbackURL() string {
157 159
158 return fmt.Sprintf("%s://%s%s%s/login/oauth/callback", scheme, domain, port, config.Admin.Path)160 return fmt.Sprintf("%s://%s%s%s/login/oauth/callback", scheme, domain, port, config.Admin.Path)
159 }161 }
162+
163+// UpdateConfig updates the config file
164+func (config *Config) UpdateConfig() error {
165+ // Marshal the config struct into YAML
166+ data, err := yaml.Marshal(config)
167+ if err != nil {
168+ return fmt.Errorf("failed to marshal config: %v", err)
169+ }
170+
171+ // Write the data to the config file (located in the build directory)
172+ err = os.WriteFile(fmt.Sprintf("%s/config.yml", config.General.BuildDir), data, 0644)
173+ if err != nil {
174+ return fmt.Errorf("failed to write to config file: %v", err)
175+ }
176+
177+ return nil
178+}
A internal/app/tokeninit.go +48 -0
@@ -0,0 +1,48 @@
1+package app
2+
3+import (
4+ "crypto/rand"
5+ "encoding/hex"
6+ "fmt"
7+)
8+
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 file
10+func (a *App) HandleNewTokenInitialization() (string, error) {
11+ // Query to check if a user with id=1 exists
12+ query := `SELECT COUNT(id) FROM users WHERE id = "1"`
13+ var count int
14+ err := a.DB.QueryRow(query).Scan(&count)
15+ if err != nil {
16+ return "", fmt.Errorf("failed to check for admin user: %v", err)
17+ }
18+
19+ // If count is 1, it means admin user already exists
20+ if count == 1 {
21+ return "", nil
22+ }
23+
24+ // If we reach here, it means admin does not exist, so we generate a token
25+ token, err := generateRandomToken(16) // 16 bytes
26+ if err != nil {
27+ return "", fmt.Errorf("failed to generate token: %v", err)
28+ }
29+
30+ fmt.Printf("Initial login token: %s\n", token)
31+
32+ // Store the token in the config file
33+ a.Config.General.GordonToken = token
34+ err = a.Config.UpdateConfig()
35+ if err != nil {
36+ return "", fmt.Errorf("failed to save config: %v", err)
37+ }
38+ return token, nil
39+}
40+
41+// generateRandomToken generates a random token of the given byte length
42+func generateRandomToken(length int) (string, error) {
43+ bytes := make([]byte, length)
44+ if _, err := rand.Read(bytes); err != nil {
45+ return "", err
46+ }
47+ return hex.EncodeToString(bytes), nil
48+}
@@ -0,0 +1,48 @@
1+package app
2+
3+import (
4+ "crypto/rand"
5+ "encoding/hex"
6+ "fmt"
7+)
8+
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 file
10+func (a *App) HandleNewTokenInitialization() (string, error) {
11+ // Query to check if a user with id=1 exists
12+ query := `SELECT COUNT(id) FROM users WHERE id = "1"`
13+ var count int
14+ err := a.DB.QueryRow(query).Scan(&count)
15+ if err != nil {
16+ return "", fmt.Errorf("failed to check for admin user: %v", err)
17+ }
18+
19+ // If count is 1, it means admin user already exists
20+ if count == 1 {
21+ return "", nil
22+ }
23+
24+ // If we reach here, it means admin does not exist, so we generate a token
25+ token, err := generateRandomToken(16) // 16 bytes
26+ if err != nil {
27+ return "", fmt.Errorf("failed to generate token: %v", err)
28+ }
29+
30+ fmt.Printf("Initial login token: %s\n", token)
31+
32+ // Store the token in the config file
33+ a.Config.General.GordonToken = token
34+ err = a.Config.UpdateConfig()
35+ if err != nil {
36+ return "", fmt.Errorf("failed to save config: %v", err)
37+ }
38+ return token, nil
39+}
40+
41+// generateRandomToken generates a random token of the given byte length
42+func generateRandomToken(length int) (string, error) {
43+ bytes := make([]byte, length)
44+ if _, err := rand.Read(bytes); err != nil {
45+ return "", err
46+ }
47+ return hex.EncodeToString(bytes), nil
48+}
M internal/database/migrate/bootstrapdb.go +1 -4
@@ -11,10 +11,7 @@ func CreateUserTable(db *sql.DB) error {
1111 CREATE TABLE users (
1212 id VARCHAR(255) PRIMARY KEY,
1313 name VARCHAR(255) UNIQUE NOT NULL,
14- email VARCHAR(255) UNIQUE NOT NULL,
15- email_verified DATETIME,
16- image VARCHAR(255),
17- is_admin BOOLEAN DEFAULT FALSE
14+ email VARCHAR(255) UNIQUE NOT NULL
1815 );`
1916
2017 _, err := db.Exec(createTableSQL)
@@ -11,10 +11,7 @@ func CreateUserTable(db *sql.DB) error {
11 CREATE TABLE users (11 CREATE TABLE users (
12 id VARCHAR(255) PRIMARY KEY,12 id VARCHAR(255) PRIMARY KEY,
13 name VARCHAR(255) UNIQUE NOT NULL,13 name VARCHAR(255) UNIQUE NOT NULL,
14- email VARCHAR(255) UNIQUE NOT NULL,14+ email VARCHAR(255) UNIQUE NOT NULL
15- email_verified DATETIME,
16- image VARCHAR(255),
17- is_admin BOOLEAN DEFAULT FALSE
18 );`15 );`
19 16
20 _, err := db.Exec(createTableSQL)17 _, err := db.Exec(createTableSQL)
A internal/database/queries/userdb.go +26 -0
@@ -0,0 +1,26 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+
6+ "github.com/bnema/gordon/internal/app"
7+)
8+
9+// Pseudo-code for all queries that will be needed on the user database
10+// User table
11+// User has many accounts (one to many) because a user can auth in many ways also have many sessions
12+
13+// 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 time
15+// TODO: prevent anyone from creating another user
16+func CreateUser(a *app.App, db *sql.DB) error {
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+}
@@ -0,0 +1,26 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+
6+ "github.com/bnema/gordon/internal/app"
7+)
8+
9+// Pseudo-code for all queries that will be needed on the user database
10+// User table
11+// User has many accounts (one to many) because a user can auth in many ways also have many sessions
12+
13+// 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 time
15+// TODO: prevent anyone from creating another user
16+func CreateUser(a *app.App, db *sql.DB) error {
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+}
M internal/httpserve/handler/oauth.go +37 -1
@@ -15,13 +15,49 @@ import (
1515 "github.com/markbates/goth/gothic"
1616 )
1717
18+// compareGordonToken compares the token from the URL query parameter with the one from the config.yml
19+func compareGordonToken(c echo.Context, a *app.App) error {
20+ urlToken := c.QueryParam("token")
21+ configToken := a.Config.General.GordonToken
22+ if urlToken != configToken {
23+ return c.Redirect(http.StatusMovedPermanently, "/")
24+ }
25+ return nil
26+}
27+
28+// Check if there is already a user in the db which means that the admin is already setup
29+func checkAdmin(a *app.App) (bool, error) {
30+ var count int
31+ err := a.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
32+ if err != nil {
33+ return false, err
34+ }
35+ if count > 0 {
36+ return true, nil
37+ }
38+ return false, nil
39+}
40+
1841 // RenderLoginPage renders the login.html template
1942 func RenderLoginPage(c echo.Context, a *app.App) error {
20- data, err := GetLocalizedData(c, a)
43+
44+ // Compare the token
45+ compareGordonToken(c, a)
46+
47+ // Check if there is already an admin in the db we cannot add another one so we redirect to the login page with a message
48+ adminExists, err := checkAdmin(a)
2149 if err != nil {
2250 return err
2351 }
2452
53+ if adminExists {
54+ return c.HTML(http.StatusOK, "<h1>User already exists</h1>")
55+ }
56+
57+ data := map[string]interface{}{
58+ "Title": "Login",
59+ }
60+
2561 // Navigate inside the fs.FS to get the template
2662 path := "html/login"
2763 rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a)
@@ -15,13 +15,49 @@ import (
15 "github.com/markbates/goth/gothic"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.yml
19+func compareGordonToken(c echo.Context, a *app.App) error {
20+ urlToken := c.QueryParam("token")
21+ configToken := a.Config.General.GordonToken
22+ if urlToken != configToken {
23+ return c.Redirect(http.StatusMovedPermanently, "/")
24+ }
25+ return nil
26+}
27+
28+// Check if there is already a user in the db which means that the admin is already setup
29+func checkAdmin(a *app.App) (bool, error) {
30+ var count int
31+ err := a.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
32+ if err != nil {
33+ return false, err
34+ }
35+ if count > 0 {
36+ return true, nil
37+ }
38+ return false, nil
39+}
40+
18 // RenderLoginPage renders the login.html template41 // RenderLoginPage renders the login.html template
19 func RenderLoginPage(c echo.Context, a *app.App) error {42 func RenderLoginPage(c echo.Context, a *app.App) error {
20- data, err := GetLocalizedData(c, a)43+
44+ // Compare the token
45+ compareGordonToken(c, a)
46+
47+ // Check if there is already an admin in the db we cannot add another one so we redirect to the login page with a message
48+ adminExists, err := checkAdmin(a)
21 if err != nil {49 if err != nil {
22 return err50 return err
23 }51 }
24 52
53+ if adminExists {
54+ return c.HTML(http.StatusOK, "<h1>User already exists</h1>")
55+ }
56+
57+ data := map[string]interface{}{
58+ "Title": "Login",
59+ }
60+
25 // Navigate inside the fs.FS to get the template61 // Navigate inside the fs.FS to get the template
26 path := "html/login"62 path := "html/login"
27 rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a)63 rendererData, err := render.GetHTMLRenderer(path, "index.gohtml", a.TemplateFS, a)

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