detached 4fc52b8

Refactor authentication, session management, and route handling

- Increase security checks for sessions and Oauth callback flow
- Add re-populate struct from db in case the server restart
- Overhaul OAuth callback, user validation, and session management in internal/httpserve/handler/oauth.go
- Revamp session validation and add new checks in internal/httpserve/middleware/requirelogin.go
- Update session cookie MaxAge in internal/httpserve/middleware/secure.go
- Reorganize admin route binding in internal/httpserve/router.go
commit
4fc52b86628a30b8f1f8c8122e99fc9ffad0c849
author
bnema · · 2023-10-18 10:56 UTC
parent
c872d87
signature
unsigned
12 files · +630 -229
M go.mod +1 -1
@@ -23,7 +23,7 @@ require (
2323 github.com/docker/go-units v0.5.0 // indirect
2424 github.com/gogo/protobuf v1.3.2 // indirect
2525 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
26- github.com/google/uuid v1.3.1 // indirect
26+ github.com/google/uuid v1.3.1 // direct
2727 github.com/gorilla/context v1.1.1 // indirect
2828 github.com/gorilla/css v1.0.0 // indirect
2929 github.com/gorilla/securecookie v1.1.1 // indirect
@@ -23,7 +23,7 @@ require (
23 github.com/docker/go-units v0.5.0 // indirect23 github.com/docker/go-units v0.5.0 // indirect
24 github.com/gogo/protobuf v1.3.2 // indirect24 github.com/gogo/protobuf v1.3.2 // indirect
25 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect25 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
26- github.com/google/uuid v1.3.1 // indirect26+ github.com/google/uuid v1.3.1 // direct
27 github.com/gorilla/context v1.1.1 // indirect27 github.com/gorilla/context v1.1.1 // indirect
28 github.com/gorilla/css v1.0.0 // indirect28 github.com/gorilla/css v1.0.0 // indirect
29 github.com/gorilla/securecookie v1.1.1 // indirect29 github.com/gorilla/securecookie v1.1.1 // indirect
M internal/app/sqliteinit.go +78 -0
@@ -6,6 +6,7 @@ import (
66 "os"
77 "path/filepath"
88
9+ "github.com/bnema/gordon/internal/db"
910 pkgsqlite "github.com/bnema/gordon/pkg/sqlite"
1011 _ "github.com/mattn/go-sqlite3"
1112 )
@@ -66,6 +67,13 @@ func InitializeDB(a *App) (*sql.DB, error) {
6667
6768 a.DB = db
6869
70+ if dbExists {
71+ // Populate the struct tables from the database
72+ if err := PopulateStructWithTables(a); err != nil {
73+ return nil, err
74+ }
75+ }
76+
6977 return db, nil
7078 }
7179
@@ -93,3 +101,73 @@ func bootstrapDB(dbPath string, app *App) error {
93101
94102 return nil
95103 }
104+
105+type DBTable interface {
106+ ScanFromRows(rows *sql.Rows) error
107+}
108+
109+type UserWrapper struct {
110+ *db.User
111+}
112+
113+type AccountWrapper struct {
114+ *db.Account
115+}
116+
117+type ProviderWrapper struct {
118+ *db.Provider
119+}
120+
121+type SessionsWrapper struct {
122+ *db.Sessions
123+}
124+
125+func populateTableFromDB(a *App, query string, table DBTable) error {
126+ rows, err := a.DB.Query(query)
127+ if err != nil {
128+ return err
129+ }
130+ defer rows.Close()
131+
132+ for rows.Next() {
133+ if err := table.ScanFromRows(rows); err != nil {
134+ return err
135+ }
136+ }
137+ return rows.Err()
138+}
139+
140+// Implementation of DBTable for User, Account, Provider, Sessions
141+func (u *UserWrapper) ScanFromRows(rows *sql.Rows) error {
142+ return rows.Scan(&u.ID, &u.Name, &u.Email)
143+}
144+
145+func (a *AccountWrapper) ScanFromRows(rows *sql.Rows) error {
146+ return rows.Scan(&a.ID, &a.UserID)
147+}
148+
149+func (p *ProviderWrapper) ScanFromRows(rows *sql.Rows) error {
150+ return rows.Scan(&p.ID, &p.AccountID, &p.Name, &p.Login, &p.AvatarURL, &p.ProfileURL, &p.Email)
151+}
152+
153+func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error {
154+ return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline)
155+}
156+
157+// PopulateStructWithTables updates db struct with tables
158+func PopulateStructWithTables(a *App) error {
159+ tables := map[string]DBTable{
160+ "SELECT id, name, email FROM user": &UserWrapper{&a.DBTables.User},
161+ "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account},
162+ "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider},
163+ "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions": &SessionsWrapper{&a.DBTables.Sessions},
164+ }
165+
166+ for query, table := range tables {
167+ if err := populateTableFromDB(a, query, table); err != nil {
168+ return err
169+ }
170+ }
171+
172+ return nil
173+}
@@ -6,6 +6,7 @@ import (
6 "os"6 "os"
7 "path/filepath"7 "path/filepath"
8 8
9+ "github.com/bnema/gordon/internal/db"
9 pkgsqlite "github.com/bnema/gordon/pkg/sqlite"10 pkgsqlite "github.com/bnema/gordon/pkg/sqlite"
10 _ "github.com/mattn/go-sqlite3"11 _ "github.com/mattn/go-sqlite3"
11 )12 )
@@ -66,6 +67,13 @@ func InitializeDB(a *App) (*sql.DB, error) {
66 67
67 a.DB = db68 a.DB = db
68 69
70+ if dbExists {
71+ // Populate the struct tables from the database
72+ if err := PopulateStructWithTables(a); err != nil {
73+ return nil, err
74+ }
75+ }
76+
69 return db, nil77 return db, nil
70 }78 }
71 79
@@ -93,3 +101,73 @@ func bootstrapDB(dbPath string, app *App) error {
93 101
94 return nil102 return nil
95 }103 }
104+
105+type DBTable interface {
106+ ScanFromRows(rows *sql.Rows) error
107+}
108+
109+type UserWrapper struct {
110+ *db.User
111+}
112+
113+type AccountWrapper struct {
114+ *db.Account
115+}
116+
117+type ProviderWrapper struct {
118+ *db.Provider
119+}
120+
121+type SessionsWrapper struct {
122+ *db.Sessions
123+}
124+
125+func populateTableFromDB(a *App, query string, table DBTable) error {
126+ rows, err := a.DB.Query(query)
127+ if err != nil {
128+ return err
129+ }
130+ defer rows.Close()
131+
132+ for rows.Next() {
133+ if err := table.ScanFromRows(rows); err != nil {
134+ return err
135+ }
136+ }
137+ return rows.Err()
138+}
139+
140+// Implementation of DBTable for User, Account, Provider, Sessions
141+func (u *UserWrapper) ScanFromRows(rows *sql.Rows) error {
142+ return rows.Scan(&u.ID, &u.Name, &u.Email)
143+}
144+
145+func (a *AccountWrapper) ScanFromRows(rows *sql.Rows) error {
146+ return rows.Scan(&a.ID, &a.UserID)
147+}
148+
149+func (p *ProviderWrapper) ScanFromRows(rows *sql.Rows) error {
150+ return rows.Scan(&p.ID, &p.AccountID, &p.Name, &p.Login, &p.AvatarURL, &p.ProfileURL, &p.Email)
151+}
152+
153+func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error {
154+ return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline)
155+}
156+
157+// PopulateStructWithTables updates db struct with tables
158+func PopulateStructWithTables(a *App) error {
159+ tables := map[string]DBTable{
160+ "SELECT id, name, email FROM user": &UserWrapper{&a.DBTables.User},
161+ "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account},
162+ "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider},
163+ "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions": &SessionsWrapper{&a.DBTables.Sessions},
164+ }
165+
166+ for query, table := range tables {
167+ if err := populateTableFromDB(a, query, table); err != nil {
168+ return err
169+ }
170+ }
171+
172+ return nil
173+}
R internal/db/struct.go → internal/db/dbstruct.go +8 -8 renamed, 68% similar
@@ -18,17 +18,17 @@ type Sessions struct {
1818 ID string `sql:"id, primary_key"`
1919 AccountID string `sql:"account_id, foreign_key=account.id"`
2020 BrowserInfo string `sql:"browser_info"`
21+ AccessToken string `sql:"access_token"`
2122 Expires string `sql:"expires"`
2223 IsOnline bool `sql:"is_online"`
2324 }
2425
2526 type Provider struct {
26- ID string `sql:"id, primary_key"`
27- AccountID string `sql:"account_id, foreign_key=account.id"`
28- Name string `sql:"name"`
29- AccessToken string `sql:"access_token"`
30- Login string `sql:"login"`
31- AvatarURL string `sql:"avatar_url"`
32- ProfileURL string `sql:"profile_url"`
33- Email string `sql:"email"`
27+ ID string `sql:"id, primary_key"`
28+ AccountID string `sql:"account_id, foreign_key=account.id"`
29+ Name string `sql:"name"`
30+ Login string `sql:"login"`
31+ AvatarURL string `sql:"avatar_url"`
32+ ProfileURL string `sql:"profile_url"`
33+ Email string `sql:"email"`
3434 }
@@ -18,17 +18,17 @@ type Sessions struct {
18 ID string `sql:"id, primary_key"`18 ID string `sql:"id, primary_key"`
19 AccountID string `sql:"account_id, foreign_key=account.id"`19 AccountID string `sql:"account_id, foreign_key=account.id"`
20 BrowserInfo string `sql:"browser_info"`20 BrowserInfo string `sql:"browser_info"`
21+ AccessToken string `sql:"access_token"`
21 Expires string `sql:"expires"`22 Expires string `sql:"expires"`
22 IsOnline bool `sql:"is_online"`23 IsOnline bool `sql:"is_online"`
23 }24 }
24 25
25 type Provider struct {26 type Provider struct {
26- ID string `sql:"id, primary_key"`27+ ID string `sql:"id, primary_key"`
27- AccountID string `sql:"account_id, foreign_key=account.id"`28+ AccountID string `sql:"account_id, foreign_key=account.id"`
28- Name string `sql:"name"`29+ Name string `sql:"name"`
29- AccessToken string `sql:"access_token"`30+ Login string `sql:"login"`
30- Login string `sql:"login"`31+ AvatarURL string `sql:"avatar_url"`
31- AvatarURL string `sql:"avatar_url"`32+ ProfileURL string `sql:"profile_url"`
32- ProfileURL string `sql:"profile_url"`33+ Email string `sql:"email"`
33- Email string `sql:"email"`
34 }34 }
A internal/db/queries/account.go +47 -0
@@ -0,0 +1,47 @@
1+package queries
2+
3+import (
4+ "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
6+)
7+
8+func createDBAccount(a *app.App) (*db.Account, error) {
9+ account := &db.Account{
10+ ID: generateUUID(),
11+ }
12+
13+ _, err := a.DB.Exec(
14+ "INSERT INTO account (id, user_id) VALUES (?, ?)",
15+ account.ID, a.DBTables.User.ID,
16+ )
17+ return account, err
18+}
19+
20+func CheckDBAccountExists(a *app.App, accountID string) (bool, error) {
21+ var exists bool
22+ err := a.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM account WHERE id = ?)", accountID).Scan(&exists)
23+ if err != nil {
24+ return false, err
25+ }
26+ return exists, nil
27+}
28+
29+func PopulateAccountFromDB(a *app.App) error {
30+ rows, err := a.DB.Query("SELECT id, user_id FROM account")
31+ if err != nil {
32+ return err
33+ }
34+ defer rows.Close()
35+
36+ for rows.Next() {
37+ account := db.Account{}
38+ err := rows.Scan(&account.ID, &account.UserID)
39+ if err != nil {
40+ return err
41+ }
42+
43+ a.DBTables.Account = account
44+ }
45+
46+ return rows.Err()
47+}
@@ -0,0 +1,47 @@
1+package queries
2+
3+import (
4+ "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
6+)
7+
8+func createDBAccount(a *app.App) (*db.Account, error) {
9+ account := &db.Account{
10+ ID: generateUUID(),
11+ }
12+
13+ _, err := a.DB.Exec(
14+ "INSERT INTO account (id, user_id) VALUES (?, ?)",
15+ account.ID, a.DBTables.User.ID,
16+ )
17+ return account, err
18+}
19+
20+func CheckDBAccountExists(a *app.App, accountID string) (bool, error) {
21+ var exists bool
22+ err := a.DB.QueryRow("SELECT EXISTS(SELECT 1 FROM account WHERE id = ?)", accountID).Scan(&exists)
23+ if err != nil {
24+ return false, err
25+ }
26+ return exists, nil
27+}
28+
29+func PopulateAccountFromDB(a *app.App) error {
30+ rows, err := a.DB.Query("SELECT id, user_id FROM account")
31+ if err != nil {
32+ return err
33+ }
34+ defer rows.Close()
35+
36+ for rows.Next() {
37+ account := db.Account{}
38+ err := rows.Scan(&account.ID, &account.UserID)
39+ if err != nil {
40+ return err
41+ }
42+
43+ a.DBTables.Account = account
44+ }
45+
46+ return rows.Err()
47+}
A internal/db/queries/provider.go +41 -0
@@ -0,0 +1,41 @@
1+package queries
2+
3+import (
4+ "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
6+)
7+
8+func createDBGitHubProvider(a *app.App, userInfo *GithubUserInfo) error {
9+ provider := &db.Provider{
10+ ID: generateUUID(),
11+ Email: userInfo.Emails[0],
12+ Login: userInfo.Login,
13+ AvatarURL: userInfo.AvatarURL,
14+ ProfileURL: userInfo.ProfileURL,
15+ Name: "GitHub",
16+ }
17+
18+ query := "INSERT INTO provider (id, account_id, name, login, avatar_url, profile_url, email) VALUES (?, ?, ?, ?, ?, ?, ?)"
19+ _, err := a.DB.Exec(query, provider.ID, a.DBTables.Account.ID, provider.Name, provider.Login, provider.AvatarURL, provider.ProfileURL, provider.Email)
20+ return err
21+}
22+
23+func PopulateProviderFromDB(a *app.App) error {
24+ rows, err := a.DB.Query("SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider")
25+ if err != nil {
26+ return err
27+ }
28+ defer rows.Close()
29+
30+ for rows.Next() {
31+ provider := db.Provider{}
32+ err := rows.Scan(&provider.ID, &provider.AccountID, &provider.Name, &provider.Login, &provider.AvatarURL, &provider.ProfileURL, &provider.Email)
33+ if err != nil {
34+ return err
35+ }
36+
37+ a.DBTables.Provider = provider
38+ }
39+
40+ return rows.Err()
41+}
@@ -0,0 +1,41 @@
1+package queries
2+
3+import (
4+ "github.com/bnema/gordon/internal/app"
5+ "github.com/bnema/gordon/internal/db"
6+)
7+
8+func createDBGitHubProvider(a *app.App, userInfo *GithubUserInfo) error {
9+ provider := &db.Provider{
10+ ID: generateUUID(),
11+ Email: userInfo.Emails[0],
12+ Login: userInfo.Login,
13+ AvatarURL: userInfo.AvatarURL,
14+ ProfileURL: userInfo.ProfileURL,
15+ Name: "GitHub",
16+ }
17+
18+ query := "INSERT INTO provider (id, account_id, name, login, avatar_url, profile_url, email) VALUES (?, ?, ?, ?, ?, ?, ?)"
19+ _, err := a.DB.Exec(query, provider.ID, a.DBTables.Account.ID, provider.Name, provider.Login, provider.AvatarURL, provider.ProfileURL, provider.Email)
20+ return err
21+}
22+
23+func PopulateProviderFromDB(a *app.App) error {
24+ rows, err := a.DB.Query("SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider")
25+ if err != nil {
26+ return err
27+ }
28+ defer rows.Close()
29+
30+ for rows.Next() {
31+ provider := db.Provider{}
32+ err := rows.Scan(&provider.ID, &provider.AccountID, &provider.Name, &provider.Login, &provider.AvatarURL, &provider.ProfileURL, &provider.Email)
33+ if err != nil {
34+ return err
35+ }
36+
37+ a.DBTables.Provider = provider
38+ }
39+
40+ return rows.Err()
41+}
A internal/db/queries/sessions.go +112 -0
@@ -0,0 +1,112 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "time"
7+
8+ "github.com/bnema/gordon/internal/app"
9+ "github.com/bnema/gordon/internal/db"
10+)
11+
12+// createSession creates a session for the user.
13+func createDBSession(a *app.App, browserInfo string, accessToken string, accountID string) error {
14+ expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
15+ sessions := &db.Sessions{
16+ ID: generateUUID(),
17+ AccessToken: accessToken,
18+ }
19+
20+ query := "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)"
21+ _, err := a.DB.Exec(query, sessions.ID, accountID, accessToken, browserInfo, expireTime, true)
22+ if err != nil {
23+ return err
24+ }
25+
26+ // Update global state here if really necessary
27+ a.DBTables.Sessions.ID = sessions.ID
28+ a.DBTables.Sessions.AccessToken = accessToken
29+ a.DBTables.Sessions.Expires = expireTime
30+
31+ return nil
32+}
33+
34+// GetDBUserSession gets the user session from the database based on the access token and browser info.
35+func GetDBUserSession(a *app.App, accessToken string, browserInfo string) (*db.Sessions, error) {
36+ query := "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_online FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?"
37+ err := a.DB.QueryRow(query, accessToken, browserInfo).Scan(&a.DBTables.Sessions.ID, &a.DBTables.Sessions.AccountID, &a.DBTables.Sessions.AccessToken, &a.DBTables.Sessions.BrowserInfo, &a.DBTables.Sessions.Expires, &a.DBTables.Sessions.IsOnline)
38+ if err != nil {
39+ if err == sql.ErrNoRows {
40+ return nil, nil
41+ }
42+ return nil, fmt.Errorf("error getting session: %w", err)
43+ }
44+
45+ return &a.DBTables.Sessions, nil
46+}
47+
48+func updateDBSession(a *app.App, accessToken string, browserInfo string) error {
49+ expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
50+ query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?"
51+ _, err := a.DB.Exec(query, accessToken, expireTime, a.DBTables.Sessions.ID)
52+ if err != nil {
53+ return err
54+ }
55+
56+ return nil
57+}
58+
59+func CreateOrUpdateDBSession(a *app.App, accessToken string, browserInfo string) error {
60+ // Check if the session exists using the access token and browser info
61+ session, err := GetDBUserSession(a, accessToken, browserInfo)
62+ if err != nil && err != sql.ErrNoRows {
63+ return fmt.Errorf("error checking for session: %w", err)
64+ }
65+
66+ // If the session exists, update the session
67+ if session != nil {
68+ err := updateDBSession(a, accessToken, browserInfo)
69+ if err != nil {
70+ return fmt.Errorf("error updating session: %w", err)
71+ }
72+ }
73+
74+ // If the session does not exist, create the session
75+ if session == nil {
76+ err := createDBSession(a, browserInfo, accessToken, a.DBTables.Account.ID)
77+ if err != nil {
78+ return fmt.Errorf("error creating session: %w", err)
79+ }
80+ }
81+
82+ return nil
83+}
84+
85+// SessionCleaner, time as a string
86+func SessionCleaner(a *app.App, currentTime string) error {
87+ query := "DELETE FROM sessions WHERE expires < ?"
88+ _, err := a.DB.Exec(query, currentTime)
89+ if err != nil {
90+ return err
91+ }
92+
93+ return nil
94+}
95+
96+func GetSessionExpiration(a *app.App, accountID string, sessionID string, currentTime time.Time) (time.Time, error) {
97+ var expirationTime string
98+ // with userID and sessionID we can get the expiration time
99+ query := "SELECT expires FROM sessions WHERE account_id = ? AND id = ?"
100+ err := a.DB.QueryRow(query, accountID, sessionID).Scan(&expirationTime)
101+ if err != nil {
102+ return currentTime, err
103+ }
104+
105+ // convert the expiration time to a time.Time
106+ expirationTimeParsed, err := time.Parse(time.RFC3339, expirationTime)
107+ if err != nil {
108+ return currentTime, err
109+ }
110+
111+ return expirationTimeParsed, nil
112+}
@@ -0,0 +1,112 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+ "time"
7+
8+ "github.com/bnema/gordon/internal/app"
9+ "github.com/bnema/gordon/internal/db"
10+)
11+
12+// createSession creates a session for the user.
13+func createDBSession(a *app.App, browserInfo string, accessToken string, accountID string) error {
14+ expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
15+ sessions := &db.Sessions{
16+ ID: generateUUID(),
17+ AccessToken: accessToken,
18+ }
19+
20+ query := "INSERT INTO sessions (id, account_id, access_token, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?, ?)"
21+ _, err := a.DB.Exec(query, sessions.ID, accountID, accessToken, browserInfo, expireTime, true)
22+ if err != nil {
23+ return err
24+ }
25+
26+ // Update global state here if really necessary
27+ a.DBTables.Sessions.ID = sessions.ID
28+ a.DBTables.Sessions.AccessToken = accessToken
29+ a.DBTables.Sessions.Expires = expireTime
30+
31+ return nil
32+}
33+
34+// GetDBUserSession gets the user session from the database based on the access token and browser info.
35+func GetDBUserSession(a *app.App, accessToken string, browserInfo string) (*db.Sessions, error) {
36+ query := "SELECT sessions.id, sessions.account_id, sessions.access_token, sessions.browser_info, sessions.expires, sessions.is_online FROM sessions INNER JOIN account ON sessions.account_id = account.id INNER JOIN provider ON account.id = provider.account_id WHERE sessions.access_token = ? AND sessions.browser_info = ?"
37+ err := a.DB.QueryRow(query, accessToken, browserInfo).Scan(&a.DBTables.Sessions.ID, &a.DBTables.Sessions.AccountID, &a.DBTables.Sessions.AccessToken, &a.DBTables.Sessions.BrowserInfo, &a.DBTables.Sessions.Expires, &a.DBTables.Sessions.IsOnline)
38+ if err != nil {
39+ if err == sql.ErrNoRows {
40+ return nil, nil
41+ }
42+ return nil, fmt.Errorf("error getting session: %w", err)
43+ }
44+
45+ return &a.DBTables.Sessions, nil
46+}
47+
48+func updateDBSession(a *app.App, accessToken string, browserInfo string) error {
49+ expireTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
50+ query := "UPDATE sessions SET access_token = ?, expires = ? WHERE id = ?"
51+ _, err := a.DB.Exec(query, accessToken, expireTime, a.DBTables.Sessions.ID)
52+ if err != nil {
53+ return err
54+ }
55+
56+ return nil
57+}
58+
59+func CreateOrUpdateDBSession(a *app.App, accessToken string, browserInfo string) error {
60+ // Check if the session exists using the access token and browser info
61+ session, err := GetDBUserSession(a, accessToken, browserInfo)
62+ if err != nil && err != sql.ErrNoRows {
63+ return fmt.Errorf("error checking for session: %w", err)
64+ }
65+
66+ // If the session exists, update the session
67+ if session != nil {
68+ err := updateDBSession(a, accessToken, browserInfo)
69+ if err != nil {
70+ return fmt.Errorf("error updating session: %w", err)
71+ }
72+ }
73+
74+ // If the session does not exist, create the session
75+ if session == nil {
76+ err := createDBSession(a, browserInfo, accessToken, a.DBTables.Account.ID)
77+ if err != nil {
78+ return fmt.Errorf("error creating session: %w", err)
79+ }
80+ }
81+
82+ return nil
83+}
84+
85+// SessionCleaner, time as a string
86+func SessionCleaner(a *app.App, currentTime string) error {
87+ query := "DELETE FROM sessions WHERE expires < ?"
88+ _, err := a.DB.Exec(query, currentTime)
89+ if err != nil {
90+ return err
91+ }
92+
93+ return nil
94+}
95+
96+func GetSessionExpiration(a *app.App, accountID string, sessionID string, currentTime time.Time) (time.Time, error) {
97+ var expirationTime string
98+ // with userID and sessionID we can get the expiration time
99+ query := "SELECT expires FROM sessions WHERE account_id = ? AND id = ?"
100+ err := a.DB.QueryRow(query, accountID, sessionID).Scan(&expirationTime)
101+ if err != nil {
102+ return currentTime, err
103+ }
104+
105+ // convert the expiration time to a time.Time
106+ expirationTimeParsed, err := time.Parse(time.RFC3339, expirationTime)
107+ if err != nil {
108+ return currentTime, err
109+ }
110+
111+ return expirationTimeParsed, nil
112+}
A internal/db/queries/user.go +140 -0
@@ -0,0 +1,140 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+
7+ "github.com/bnema/gordon/internal/app"
8+ "github.com/bnema/gordon/internal/db"
9+ "github.com/google/uuid"
10+)
11+
12+func generateUUID() string {
13+ return uuid.New().String()
14+}
15+
16+// UserInfo holds the essential information for a Github user.
17+type GithubUserInfo struct {
18+ Login string `json:"login"`
19+ AvatarURL string `json:"avatar_url"`
20+ ProfileURL string `json:"html_url"`
21+ Emails []string
22+}
23+
24+// CreateUser creates a new user along with the associated account, provider, and session.
25+func CreateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) error {
26+ // Check if a user already exists. If so, return an error.
27+ if exists, err := CheckDBUserExists(a); err != nil || exists {
28+ return fmt.Errorf("error checking user or user already exists: %v", err)
29+ }
30+
31+ user, err := createDBUser(a, userInfo)
32+ if err != nil {
33+ return err
34+ }
35+
36+ fmt.Println("user", user)
37+
38+ // update the global state
39+ a.DBTables.User.ID = user.ID
40+ a.DBTables.User.Name = user.Name
41+ a.DBTables.User.Email = user.Email
42+
43+ account, err := createDBAccount(a)
44+ if err != nil {
45+ return err
46+ }
47+
48+ // update the global state
49+ a.DBTables.Account.ID = account.ID
50+ a.DBTables.Account.UserID = account.UserID
51+
52+ err = createDBGitHubProvider(a, userInfo)
53+ if err != nil {
54+ return err
55+ }
56+
57+ return createDBSession(a, browserInfo, accessToken, account.ID)
58+}
59+
60+func createDBUser(a *app.App, userInfo *GithubUserInfo) (*db.User, error) {
61+ user := &db.User{
62+ ID: generateUUID(),
63+ Name: userInfo.Login,
64+ Email: userInfo.Emails[0],
65+ }
66+
67+ _, err := a.DB.Exec(
68+ "INSERT INTO user (id, name, email) VALUES (?, ?, ?)",
69+ user.ID, user.Name, user.Email,
70+ )
71+ return user, err
72+}
73+
74+func CheckDBUserExists(a *app.App) (bool, error) {
75+ var userID string
76+ err := a.DB.QueryRow("SELECT id FROM user").Scan(&userID)
77+ if err != nil {
78+ if err == sql.ErrNoRows {
79+ return false, nil
80+ }
81+ return false, err
82+ }
83+
84+ if userID != "" {
85+ return true, nil
86+ }
87+
88+ return false, nil
89+}
90+
91+// If there is already a user, we compare the login and email to see if it is the same user
92+func CheckDBUserIsGood(a *app.App, userInfo *GithubUserInfo) (bool, error) {
93+ login := userInfo.Login
94+ email := userInfo.Emails[0]
95+
96+ // Check if the user exists based on github login and email
97+ query := "SELECT user.id, user.name, user.email FROM user INNER JOIN account ON user.id = account.user_id INNER JOIN provider ON account.id = provider.account_id WHERE provider.login = ? AND provider.email = ?"
98+ err := a.DB.QueryRow(query, login, email).Scan(&a.DBTables.User.ID, &a.DBTables.User.Name, &a.DBTables.User.Email)
99+ if err != nil {
100+ if err == sql.ErrNoRows {
101+ return false, nil
102+ }
103+ return false, err
104+ }
105+
106+ if a.DBTables.User.ID != "" {
107+ return true, nil
108+ }
109+
110+ return false, nil
111+}
112+
113+func UpdateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) (*db.User, error) {
114+
115+ err := CreateOrUpdateDBSession(a, accessToken, browserInfo)
116+ if err != nil {
117+ return nil, fmt.Errorf("could not create or update session: %w", err)
118+ }
119+
120+ user := a.DBTables.User
121+
122+ query := "SELECT user.id, user.name, user.email FROM user INNER JOIN account ON user.id = account.user_id INNER JOIN provider ON account.id = provider.account_id WHERE provider.login = ? AND provider.email = ?"
123+ err = a.DB.QueryRow(query, userInfo.Login, userInfo.Emails[0]).Scan(&user.ID, &user.Name, &user.Email)
124+ if err != nil {
125+ return nil, err
126+ }
127+
128+ return &user, nil
129+
130+}
131+
132+func GetAccountCount(a *app.App) (int, error) {
133+ var count int
134+ err := a.DB.QueryRow("SELECT COUNT(*) FROM account").Scan(&count)
135+ if err != nil {
136+ return 0, err
137+ }
138+
139+ return count, nil
140+}
@@ -0,0 +1,140 @@
1+package queries
2+
3+import (
4+ "database/sql"
5+ "fmt"
6+
7+ "github.com/bnema/gordon/internal/app"
8+ "github.com/bnema/gordon/internal/db"
9+ "github.com/google/uuid"
10+)
11+
12+func generateUUID() string {
13+ return uuid.New().String()
14+}
15+
16+// UserInfo holds the essential information for a Github user.
17+type GithubUserInfo struct {
18+ Login string `json:"login"`
19+ AvatarURL string `json:"avatar_url"`
20+ ProfileURL string `json:"html_url"`
21+ Emails []string
22+}
23+
24+// CreateUser creates a new user along with the associated account, provider, and session.
25+func CreateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) error {
26+ // Check if a user already exists. If so, return an error.
27+ if exists, err := CheckDBUserExists(a); err != nil || exists {
28+ return fmt.Errorf("error checking user or user already exists: %v", err)
29+ }
30+
31+ user, err := createDBUser(a, userInfo)
32+ if err != nil {
33+ return err
34+ }
35+
36+ fmt.Println("user", user)
37+
38+ // update the global state
39+ a.DBTables.User.ID = user.ID
40+ a.DBTables.User.Name = user.Name
41+ a.DBTables.User.Email = user.Email
42+
43+ account, err := createDBAccount(a)
44+ if err != nil {
45+ return err
46+ }
47+
48+ // update the global state
49+ a.DBTables.Account.ID = account.ID
50+ a.DBTables.Account.UserID = account.UserID
51+
52+ err = createDBGitHubProvider(a, userInfo)
53+ if err != nil {
54+ return err
55+ }
56+
57+ return createDBSession(a, browserInfo, accessToken, account.ID)
58+}
59+
60+func createDBUser(a *app.App, userInfo *GithubUserInfo) (*db.User, error) {
61+ user := &db.User{
62+ ID: generateUUID(),
63+ Name: userInfo.Login,
64+ Email: userInfo.Emails[0],
65+ }
66+
67+ _, err := a.DB.Exec(
68+ "INSERT INTO user (id, name, email) VALUES (?, ?, ?)",
69+ user.ID, user.Name, user.Email,
70+ )
71+ return user, err
72+}
73+
74+func CheckDBUserExists(a *app.App) (bool, error) {
75+ var userID string
76+ err := a.DB.QueryRow("SELECT id FROM user").Scan(&userID)
77+ if err != nil {
78+ if err == sql.ErrNoRows {
79+ return false, nil
80+ }
81+ return false, err
82+ }
83+
84+ if userID != "" {
85+ return true, nil
86+ }
87+
88+ return false, nil
89+}
90+
91+// If there is already a user, we compare the login and email to see if it is the same user
92+func CheckDBUserIsGood(a *app.App, userInfo *GithubUserInfo) (bool, error) {
93+ login := userInfo.Login
94+ email := userInfo.Emails[0]
95+
96+ // Check if the user exists based on github login and email
97+ query := "SELECT user.id, user.name, user.email FROM user INNER JOIN account ON user.id = account.user_id INNER JOIN provider ON account.id = provider.account_id WHERE provider.login = ? AND provider.email = ?"
98+ err := a.DB.QueryRow(query, login, email).Scan(&a.DBTables.User.ID, &a.DBTables.User.Name, &a.DBTables.User.Email)
99+ if err != nil {
100+ if err == sql.ErrNoRows {
101+ return false, nil
102+ }
103+ return false, err
104+ }
105+
106+ if a.DBTables.User.ID != "" {
107+ return true, nil
108+ }
109+
110+ return false, nil
111+}
112+
113+func UpdateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) (*db.User, error) {
114+
115+ err := CreateOrUpdateDBSession(a, accessToken, browserInfo)
116+ if err != nil {
117+ return nil, fmt.Errorf("could not create or update session: %w", err)
118+ }
119+
120+ user := a.DBTables.User
121+
122+ query := "SELECT user.id, user.name, user.email FROM user INNER JOIN account ON user.id = account.user_id INNER JOIN provider ON account.id = provider.account_id WHERE provider.login = ? AND provider.email = ?"
123+ err = a.DB.QueryRow(query, userInfo.Login, userInfo.Emails[0]).Scan(&user.ID, &user.Name, &user.Email)
124+ if err != nil {
125+ return nil, err
126+ }
127+
128+ return &user, nil
129+
130+}
131+
132+func GetAccountCount(a *app.App) (int, error) {
133+ var count int
134+ err := a.DB.QueryRow("SELECT COUNT(*) FROM account").Scan(&count)
135+ if err != nil {
136+ return 0, err
137+ }
138+
139+ return count, nil
140+}
D internal/db/queries/userqueries.go +0 -164
@@ -1,164 +0,0 @@
1-package queries
2-
3-import (
4- "database/sql"
5- "fmt"
6- "time"
7-
8- "github.com/bnema/gordon/internal/app"
9- "github.com/bnema/gordon/internal/db"
10- "github.com/google/uuid"
11-)
12-
13-func generateUUID() string {
14- return uuid.New().String()
15-}
16-
17-// UserInfo holds the essential information for a Github user.
18-type GithubUserInfo struct {
19- Login string `json:"login"`
20- AvatarURL string `json:"avatar_url"`
21- ProfileURL string `json:"html_url"`
22- Emails []string
23-}
24-
25-// CreateUser creates a new user along with the associated account, provider, and session.
26-func CreateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) error {
27- // Check if a user already exists. If so, return an error.
28- if exists, err := CheckDBUserExists(a); err != nil || exists {
29- return fmt.Errorf("error checking user or user already exists: %v", err)
30- }
31-
32- user, err := createDBUser(a, userInfo)
33- if err != nil {
34- return err
35- }
36- a.DBTables.User.ID = user.ID
37-
38- account, err := createDBAccount(a)
39- if err != nil {
40- return err
41- }
42- a.DBTables.Account.ID = account.ID
43-
44- err = createDBGitHubProvider(a, accessToken, userInfo)
45- if err != nil {
46- return err
47- }
48-
49- return createDBSession(a, browserInfo)
50-}
51-
52-func createDBUser(a *app.App, userInfo *GithubUserInfo) (*db.User, error) {
53- user := &db.User{
54- ID: generateUUID(),
55- Name: userInfo.Login,
56- Email: userInfo.Emails[0],
57- }
58-
59- _, err := a.DB.Exec(
60- "INSERT INTO user (id, name, email) VALUES (?, ?, ?)",
61- user.ID, user.Name, user.Email,
62- )
63- return user, err
64-}
65-
66-func createDBAccount(a *app.App) (*db.Account, error) {
67- account := &db.Account{
68- ID: generateUUID(),
69- }
70-
71- _, err := a.DB.Exec(
72- "INSERT INTO account (id, user_id) VALUES (?, ?)",
73- account.ID, a.DBTables.User.ID,
74- )
75- return account, err
76-}
77-
78-func createDBGitHubProvider(a *app.App, accessToken string, userInfo *GithubUserInfo) error {
79- provider := &db.Provider{
80- ID: generateUUID(),
81- AccessToken: accessToken,
82- Email: userInfo.Emails[0],
83- Login: userInfo.Login,
84- AvatarURL: userInfo.AvatarURL,
85- ProfileURL: userInfo.ProfileURL,
86- }
87-
88- query := "INSERT INTO provider (id, account_id, name, access_token, login, avatar_url, profile_url, email) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
89- _, err := a.DB.Exec(query, provider.ID, a.DBTables.Account.ID, "GitHub", provider.AccessToken, provider.Login, provider.AvatarURL, provider.ProfileURL, provider.Email)
90- return err
91-}
92-
93-// createSession creates a session for the user.
94-func createDBSession(a *app.App, browserInfo string) error {
95- sessions := &db.Sessions{
96- ID: generateUUID(),
97- }
98-
99- query := "INSERT INTO sessions (id, account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)"
100- _, err := a.DB.Exec(query, sessions.ID, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true)
101- a.DBTables.Sessions.ID = sessions.ID
102- return err
103-}
104-
105-func CreateOrUpdateDBSession(a *app.App, accessToken string, browserInfo string) error {
106- existingSessionID := a.DBTables.Sessions.ID
107-
108- queryFindExistingSession := "SELECT id FROM sessions WHERE account_id = ? AND browser_info = ?"
109- err := a.DB.QueryRow(queryFindExistingSession, a.DBTables.Account.ID, browserInfo).Scan(&existingSessionID)
110- if err != nil {
111- return err
112- }
113-
114- // Calculate the new expiry time
115- newExpiryTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
116-
117- if existingSessionID != "" {
118- // Update the session if it exists
119- _, err = a.DB.Exec("UPDATE sessions SET expires = ?, is_online = ? WHERE id = ?", newExpiryTime, true, existingSessionID)
120- if err != nil {
121- return err
122- }
123- } else {
124- // Create a new session if it doesn't exist
125- queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)"
126- _, err = a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true)
127- if err != nil {
128- return err
129- }
130- }
131-
132- return nil
133-}
134-
135-func CheckDBAccountAccessToken(a *app.App, accessToken string) (bool, error) {
136- var existingAccessToken string
137- err := a.DB.QueryRow("SELECT access_token FROM provider WHERE name = ?", "GitHub").Scan(&existingAccessToken)
138- if err != nil {
139- return false, err
140- }
141-
142- if existingAccessToken == accessToken {
143- return true, nil
144- }
145-
146- return false, nil
147-}
148-
149-func CheckDBUserExists(a *app.App) (bool, error) {
150- var userID string
151- err := a.DB.QueryRow("SELECT id FROM user").Scan(&userID)
152- if err != nil {
153- if err == sql.ErrNoRows {
154- return false, nil
155- }
156- return false, err
157- }
158-
159- if userID != "" {
160- return true, nil
161- }
162-
163- return false, nil
164-}
@@ -1,164 +0,0 @@
1-package queries
2-
3-import (
4- "database/sql"
5- "fmt"
6- "time"
7-
8- "github.com/bnema/gordon/internal/app"
9- "github.com/bnema/gordon/internal/db"
10- "github.com/google/uuid"
11-)
12-
13-func generateUUID() string {
14- return uuid.New().String()
15-}
16-
17-// UserInfo holds the essential information for a Github user.
18-type GithubUserInfo struct {
19- Login string `json:"login"`
20- AvatarURL string `json:"avatar_url"`
21- ProfileURL string `json:"html_url"`
22- Emails []string
23-}
24-
25-// CreateUser creates a new user along with the associated account, provider, and session.
26-func CreateUser(a *app.App, accessToken string, browserInfo string, userInfo *GithubUserInfo) error {
27- // Check if a user already exists. If so, return an error.
28- if exists, err := CheckDBUserExists(a); err != nil || exists {
29- return fmt.Errorf("error checking user or user already exists: %v", err)
30- }
31-
32- user, err := createDBUser(a, userInfo)
33- if err != nil {
34- return err
35- }
36- a.DBTables.User.ID = user.ID
37-
38- account, err := createDBAccount(a)
39- if err != nil {
40- return err
41- }
42- a.DBTables.Account.ID = account.ID
43-
44- err = createDBGitHubProvider(a, accessToken, userInfo)
45- if err != nil {
46- return err
47- }
48-
49- return createDBSession(a, browserInfo)
50-}
51-
52-func createDBUser(a *app.App, userInfo *GithubUserInfo) (*db.User, error) {
53- user := &db.User{
54- ID: generateUUID(),
55- Name: userInfo.Login,
56- Email: userInfo.Emails[0],
57- }
58-
59- _, err := a.DB.Exec(
60- "INSERT INTO user (id, name, email) VALUES (?, ?, ?)",
61- user.ID, user.Name, user.Email,
62- )
63- return user, err
64-}
65-
66-func createDBAccount(a *app.App) (*db.Account, error) {
67- account := &db.Account{
68- ID: generateUUID(),
69- }
70-
71- _, err := a.DB.Exec(
72- "INSERT INTO account (id, user_id) VALUES (?, ?)",
73- account.ID, a.DBTables.User.ID,
74- )
75- return account, err
76-}
77-
78-func createDBGitHubProvider(a *app.App, accessToken string, userInfo *GithubUserInfo) error {
79- provider := &db.Provider{
80- ID: generateUUID(),
81- AccessToken: accessToken,
82- Email: userInfo.Emails[0],
83- Login: userInfo.Login,
84- AvatarURL: userInfo.AvatarURL,
85- ProfileURL: userInfo.ProfileURL,
86- }
87-
88- query := "INSERT INTO provider (id, account_id, name, access_token, login, avatar_url, profile_url, email) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"
89- _, err := a.DB.Exec(query, provider.ID, a.DBTables.Account.ID, "GitHub", provider.AccessToken, provider.Login, provider.AvatarURL, provider.ProfileURL, provider.Email)
90- return err
91-}
92-
93-// createSession creates a session for the user.
94-func createDBSession(a *app.App, browserInfo string) error {
95- sessions := &db.Sessions{
96- ID: generateUUID(),
97- }
98-
99- query := "INSERT INTO sessions (id, account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)"
100- _, err := a.DB.Exec(query, sessions.ID, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true)
101- a.DBTables.Sessions.ID = sessions.ID
102- return err
103-}
104-
105-func CreateOrUpdateDBSession(a *app.App, accessToken string, browserInfo string) error {
106- existingSessionID := a.DBTables.Sessions.ID
107-
108- queryFindExistingSession := "SELECT id FROM sessions WHERE account_id = ? AND browser_info = ?"
109- err := a.DB.QueryRow(queryFindExistingSession, a.DBTables.Account.ID, browserInfo).Scan(&existingSessionID)
110- if err != nil {
111- return err
112- }
113-
114- // Calculate the new expiry time
115- newExpiryTime := time.Now().Add(time.Hour * 24).Format(time.RFC3339)
116-
117- if existingSessionID != "" {
118- // Update the session if it exists
119- _, err = a.DB.Exec("UPDATE sessions SET expires = ?, is_online = ? WHERE id = ?", newExpiryTime, true, existingSessionID)
120- if err != nil {
121- return err
122- }
123- } else {
124- // Create a new session if it doesn't exist
125- queryInsertSession := "INSERT INTO sessions (account_id, browser_info, expires, is_online) VALUES (?, ?, ?, ?, ?)"
126- _, err = a.DB.Exec(queryInsertSession, a.DBTables.Account.ID, browserInfo, time.Now().Add(time.Hour*24).Format(time.RFC3339), true)
127- if err != nil {
128- return err
129- }
130- }
131-
132- return nil
133-}
134-
135-func CheckDBAccountAccessToken(a *app.App, accessToken string) (bool, error) {
136- var existingAccessToken string
137- err := a.DB.QueryRow("SELECT access_token FROM provider WHERE name = ?", "GitHub").Scan(&existingAccessToken)
138- if err != nil {
139- return false, err
140- }
141-
142- if existingAccessToken == accessToken {
143- return true, nil
144- }
145-
146- return false, nil
147-}
148-
149-func CheckDBUserExists(a *app.App) (bool, error) {
150- var userID string
151- err := a.DB.QueryRow("SELECT id FROM user").Scan(&userID)
152- if err != nil {
153- if err == sql.ErrNoRows {
154- return false, nil
155- }
156- return false, err
157- }
158-
159- if userID != "" {
160- return true, nil
161- }
162-
163- return false, nil
164-}
M internal/httpserve/handler/oauth.go +114 -31
@@ -12,27 +12,27 @@ import (
1212 "github.com/bnema/gordon/internal/db"
1313 "github.com/bnema/gordon/internal/db/queries"
1414 "github.com/bnema/gordon/internal/templates/render"
15+ "github.com/gorilla/sessions"
1516 _ "github.com/joho/godotenv/autoload"
1617 "github.com/labstack/echo-contrib/session"
1718 "github.com/labstack/echo/v4"
1819 )
1920
21+var urlToken string
22+
2023 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml
2124 func compareGordonToken(c echo.Context, a *app.App) error {
22- urlToken := c.QueryParam("token")
2325 configToken := a.Config.General.GordonToken
2426 if urlToken != configToken {
25- return c.Redirect(http.StatusMovedPermanently, "/")
27+ // if token is not present or does not match the one from the config.yml
28+ return fmt.Errorf("token is not valid")
2629 }
2730 return nil
2831 }
2932
3033 // RenderLoginPage renders the login.html template
3134 func RenderLoginPage(c echo.Context, a *app.App) error {
32-
33- // Compare the token
34- compareGordonToken(c, a)
35-
35+ urlToken = c.QueryParam("token")
3636 data := map[string]interface{}{
3737 "Title": "Login",
3838 }
@@ -58,6 +58,7 @@ func StartOAuthGithub(c echo.Context, a *app.App) error {
5858 if err != nil {
5959 return err
6060 }
61+
6162 //Initiate the Github OAuth flow
6263 clientID := os.Getenv("GITHUB_APP_ID")
6364 redirectDomain := a.Config.GenerateOauthCallbackURL()
@@ -77,53 +78,135 @@ type Sessions struct {
7778 }
7879
7980 func OAuthCallback(c echo.Context, a *app.App) error {
81+ redirectPath := a.Config.Admin.Path
8082
81- accessToken := c.QueryParam("access_token")
82- encodedState := c.QueryParam("state")
83-
84- // Decode the state parameter to validate the original redirectDomain
85- _, err := base64.StdEncoding.DecodeString(encodedState)
83+ accessToken, encodedState, err := parseQueryParams(c)
8684 if err != nil {
87- return c.String(http.StatusBadRequest, "Invalid state parameter")
85+ return c.String(http.StatusBadRequest, err.Error())
8886 }
8987
90- sess, err := session.Get("session", c)
91- if err != nil {
92- return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
88+ // update the struct with the new access token
89+ a.DBTables.Sessions.AccessToken = accessToken
90+
91+ // Compare the state parameter with the redirect domain
92+ redirectDomain := a.Config.GenerateOauthCallbackURL()
93+ encodedRedirectDomain := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
94+ if encodedState != encodedRedirectDomain {
95+ return c.String(http.StatusBadRequest, "invalid state parameter")
9396 }
94- // Set the user as authenticated
95- sess.Values["authenticated"] = true
96- sess.Values["access_token"] = accessToken
97- sess.Values["expires"] = 604800
98- if err = sess.Save(c.Request(), c.Response()); err != nil {
99- return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
97+
98+ sess, err := getSession(c)
99+ if err != nil {
100+ return err
100101 }
101102
102103 browserInfo := c.Request().UserAgent()
103- // print github user info
104104 userInfo, err := githubGetUserDetails(c, a)
105105 if err != nil {
106106 return err
107107 }
108108
109- // Check if the user exists
110- // if not, create it
111- // if yes, update the access token and create or update the session
112- // If the user does not exist, create it
109+ user, err := handleUser(c, a, accessToken, browserInfo, userInfo)
110+ if err != nil {
111+ return err
112+ }
113+
114+ err = setSessionValues(c, sess, user, a.DBTables.Account.ID, a.DBTables.Sessions.Expires, a.DBTables.Sessions.ID)
115+ if err != nil {
116+ return err
117+ }
118+
119+ return c.Redirect(http.StatusFound, redirectPath)
120+}
121+
122+func parseQueryParams(c echo.Context) (string, string, error) {
123+ accessToken := c.QueryParam("access_token")
124+ encodedState := c.QueryParam("state")
125+
126+ _, err := base64.StdEncoding.DecodeString(encodedState)
127+ if err != nil {
128+ return "", "", fmt.Errorf("invalid state parameter")
129+ }
130+
131+ return accessToken, encodedState, nil
132+}
133+
134+func getSession(c echo.Context) (*sessions.Session, error) {
135+ sess, err := session.Get("session", c)
136+ if err != nil {
137+ return nil, echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
138+ }
139+ return sess, nil
140+}
141+
142+func handleUser(c echo.Context, a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
113143 userExists, err := queries.CheckDBUserExists(a)
114144 if err != nil {
115- return fmt.Errorf("could not check if user exists: %w", err)
145+ return nil, fmt.Errorf("could not check if user exists: %w", err)
116146 }
147+
117148 if !userExists {
118- err := queries.CreateUser(a, accessToken, browserInfo, userInfo)
149+ // if it is a new user creation we compare the gordon token
150+ err := compareGordonToken(c, a)
119151 if err != nil {
120- return fmt.Errorf("could not create user: %w", err)
152+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is empty or not valid")
153+ }
154+
155+ return createUser(a, accessToken, browserInfo, userInfo)
156+ }
157+
158+ if userExists {
159+ // if it is an existing user we compare the login and email to see if it is the same user
160+ isGoodUser, err := queries.CheckDBUserIsGood(a, userInfo)
161+ if err != nil {
162+ return nil, fmt.Errorf("could not check if user is good: %w", err)
163+ }
164+
165+ if !isGoodUser {
166+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
167+ }
168+
169+ if isGoodUser {
170+ // Update the user
171+ updatedUser, err := updateUser(a, accessToken, browserInfo, userInfo)
172+ if err != nil {
173+ return nil, fmt.Errorf("could not update user: %w", err)
174+ }
175+ return updatedUser, nil
121176 }
122- } else {
177+ }
178+
179+ return nil, fmt.Errorf("could not handle user")
180+}
181+
182+func createUser(a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
183+ err := queries.CreateUser(a, accessToken, browserInfo, userInfo)
184+ if err != nil {
185+ return nil, fmt.Errorf("could not create user: %w", err)
186+ }
187+ return &a.DBTables.User, nil
188+}
123189
190+func updateUser(a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
191+ user, err := queries.UpdateUser(a, accessToken, browserInfo, userInfo)
192+ if err != nil {
193+ return nil, fmt.Errorf("could not update user: %w", err)
194+ }
195+ return user, nil
196+}
197+
198+func setSessionValues(c echo.Context, sess *sessions.Session, user *db.User, accountID string, expires string, sessionID string) error {
199+ sess.Values["authenticated"] = true
200+ sess.Values["accountID"] = accountID
201+ sess.Values["expires"] = expires
202+ sess.Values["sessionID"] = sessionID
203+
204+ if err := sess.Save(c.Request(), c.Response()); err != nil {
205+ return fmt.Errorf("could not save session: %w", err)
124206 }
125- return c.Redirect(http.StatusFound, "/admin")
207+ return nil
126208 }
209+
127210 func fetchGithubAPI(url string, authHeader string, result interface{}) error {
128211 req, err := http.NewRequest("GET", url, nil)
129212 if err != nil {
@@ -12,27 +12,27 @@ import (
12 "github.com/bnema/gordon/internal/db"12 "github.com/bnema/gordon/internal/db"
13 "github.com/bnema/gordon/internal/db/queries"13 "github.com/bnema/gordon/internal/db/queries"
14 "github.com/bnema/gordon/internal/templates/render"14 "github.com/bnema/gordon/internal/templates/render"
15+ "github.com/gorilla/sessions"
15 _ "github.com/joho/godotenv/autoload"16 _ "github.com/joho/godotenv/autoload"
16 "github.com/labstack/echo-contrib/session"17 "github.com/labstack/echo-contrib/session"
17 "github.com/labstack/echo/v4"18 "github.com/labstack/echo/v4"
18 )19 )
19 20
21+var urlToken string
22+
20 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml23 // compareGordonToken compares the token from the URL query parameter with the one from the config.yml
21 func compareGordonToken(c echo.Context, a *app.App) error {24 func compareGordonToken(c echo.Context, a *app.App) error {
22- urlToken := c.QueryParam("token")
23 configToken := a.Config.General.GordonToken25 configToken := a.Config.General.GordonToken
24 if urlToken != configToken {26 if urlToken != configToken {
25- return c.Redirect(http.StatusMovedPermanently, "/")27+ // if token is not present or does not match the one from the config.yml
28+ return fmt.Errorf("token is not valid")
26 }29 }
27 return nil30 return nil
28 }31 }
29 32
30 // RenderLoginPage renders the login.html template33 // RenderLoginPage renders the login.html template
31 func RenderLoginPage(c echo.Context, a *app.App) error {34 func RenderLoginPage(c echo.Context, a *app.App) error {
32-35+ urlToken = c.QueryParam("token")
33- // Compare the token
34- compareGordonToken(c, a)
35-
36 data := map[string]interface{}{36 data := map[string]interface{}{
37 "Title": "Login",37 "Title": "Login",
38 }38 }
@@ -58,6 +58,7 @@ func StartOAuthGithub(c echo.Context, a *app.App) error {
58 if err != nil {58 if err != nil {
59 return err59 return err
60 }60 }
61+
61 //Initiate the Github OAuth flow62 //Initiate the Github OAuth flow
62 clientID := os.Getenv("GITHUB_APP_ID")63 clientID := os.Getenv("GITHUB_APP_ID")
63 redirectDomain := a.Config.GenerateOauthCallbackURL()64 redirectDomain := a.Config.GenerateOauthCallbackURL()
@@ -77,53 +78,135 @@ type Sessions struct {
77 }78 }
78 79
79 func OAuthCallback(c echo.Context, a *app.App) error {80 func OAuthCallback(c echo.Context, a *app.App) error {
81+ redirectPath := a.Config.Admin.Path
80 82
81- accessToken := c.QueryParam("access_token")83+ accessToken, encodedState, err := parseQueryParams(c)
82- encodedState := c.QueryParam("state")
83-
84- // Decode the state parameter to validate the original redirectDomain
85- _, err := base64.StdEncoding.DecodeString(encodedState)
86 if err != nil {84 if err != nil {
87- return c.String(http.StatusBadRequest, "Invalid state parameter")85+ return c.String(http.StatusBadRequest, err.Error())
88 }86 }
89 87
90- sess, err := session.Get("session", c)88+ // update the struct with the new access token
91- if err != nil {89+ a.DBTables.Sessions.AccessToken = accessToken
92- return echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")90+
91+ // Compare the state parameter with the redirect domain
92+ redirectDomain := a.Config.GenerateOauthCallbackURL()
93+ encodedRedirectDomain := base64.StdEncoding.EncodeToString([]byte("redirectDomain:" + redirectDomain))
94+ if encodedState != encodedRedirectDomain {
95+ return c.String(http.StatusBadRequest, "invalid state parameter")
93 }96 }
94- // Set the user as authenticated97+
95- sess.Values["authenticated"] = true98+ sess, err := getSession(c)
96- sess.Values["access_token"] = accessToken99+ if err != nil {
97- sess.Values["expires"] = 604800100+ return err
98- if err = sess.Save(c.Request(), c.Response()); err != nil {
99- return echo.NewHTTPError(http.StatusInternalServerError, "Could not save session")
100 }101 }
101 102
102 browserInfo := c.Request().UserAgent()103 browserInfo := c.Request().UserAgent()
103- // print github user info
104 userInfo, err := githubGetUserDetails(c, a)104 userInfo, err := githubGetUserDetails(c, a)
105 if err != nil {105 if err != nil {
106 return err106 return err
107 }107 }
108 108
109- // Check if the user exists109+ user, err := handleUser(c, a, accessToken, browserInfo, userInfo)
110- // if not, create it110+ if err != nil {
111- // if yes, update the access token and create or update the session111+ return err
112- // If the user does not exist, create it112+ }
113+
114+ err = setSessionValues(c, sess, user, a.DBTables.Account.ID, a.DBTables.Sessions.Expires, a.DBTables.Sessions.ID)
115+ if err != nil {
116+ return err
117+ }
118+
119+ return c.Redirect(http.StatusFound, redirectPath)
120+}
121+
122+func parseQueryParams(c echo.Context) (string, string, error) {
123+ accessToken := c.QueryParam("access_token")
124+ encodedState := c.QueryParam("state")
125+
126+ _, err := base64.StdEncoding.DecodeString(encodedState)
127+ if err != nil {
128+ return "", "", fmt.Errorf("invalid state parameter")
129+ }
130+
131+ return accessToken, encodedState, nil
132+}
133+
134+func getSession(c echo.Context) (*sessions.Session, error) {
135+ sess, err := session.Get("session", c)
136+ if err != nil {
137+ return nil, echo.NewHTTPError(http.StatusInternalServerError, "Could not get session")
138+ }
139+ return sess, nil
140+}
141+
142+func handleUser(c echo.Context, a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
113 userExists, err := queries.CheckDBUserExists(a)143 userExists, err := queries.CheckDBUserExists(a)
114 if err != nil {144 if err != nil {
115- return fmt.Errorf("could not check if user exists: %w", err)145+ return nil, fmt.Errorf("could not check if user exists: %w", err)
116 }146 }
147+
117 if !userExists {148 if !userExists {
118- err := queries.CreateUser(a, accessToken, browserInfo, userInfo)149+ // if it is a new user creation we compare the gordon token
150+ err := compareGordonToken(c, a)
119 if err != nil {151 if err != nil {
120- return fmt.Errorf("could not create user: %w", err)152+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "Token is empty or not valid")
153+ }
154+
155+ return createUser(a, accessToken, browserInfo, userInfo)
156+ }
157+
158+ if userExists {
159+ // if it is an existing user we compare the login and email to see if it is the same user
160+ isGoodUser, err := queries.CheckDBUserIsGood(a, userInfo)
161+ if err != nil {
162+ return nil, fmt.Errorf("could not check if user is good: %w", err)
163+ }
164+
165+ if !isGoodUser {
166+ return nil, echo.NewHTTPError(http.StatusUnauthorized, "Unauthorized")
167+ }
168+
169+ if isGoodUser {
170+ // Update the user
171+ updatedUser, err := updateUser(a, accessToken, browserInfo, userInfo)
172+ if err != nil {
173+ return nil, fmt.Errorf("could not update user: %w", err)
174+ }
175+ return updatedUser, nil
121 }176 }
122- } else {177+ }
178+
179+ return nil, fmt.Errorf("could not handle user")
180+}
181+
182+func createUser(a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
183+ err := queries.CreateUser(a, accessToken, browserInfo, userInfo)
184+ if err != nil {
185+ return nil, fmt.Errorf("could not create user: %w", err)
186+ }
187+ return &a.DBTables.User, nil
188+}
123 189
190+func updateUser(a *app.App, accessToken, browserInfo string, userInfo *queries.GithubUserInfo) (*db.User, error) {
191+ user, err := queries.UpdateUser(a, accessToken, browserInfo, userInfo)
192+ if err != nil {
193+ return nil, fmt.Errorf("could not update user: %w", err)
194+ }
195+ return user, nil
196+}
197+
198+func setSessionValues(c echo.Context, sess *sessions.Session, user *db.User, accountID string, expires string, sessionID string) error {
199+ sess.Values["authenticated"] = true
200+ sess.Values["accountID"] = accountID
201+ sess.Values["expires"] = expires
202+ sess.Values["sessionID"] = sessionID
203+
204+ if err := sess.Save(c.Request(), c.Response()); err != nil {
205+ return fmt.Errorf("could not save session: %w", err)
124 }206 }
125- return c.Redirect(http.StatusFound, "/admin")207+ return nil
126 }208 }
209+
127 func fetchGithubAPI(url string, authHeader string, result interface{}) error {210 func fetchGithubAPI(url string, authHeader string, result interface{}) error {
128 req, err := http.NewRequest("GET", url, nil)211 req, err := http.NewRequest("GET", url, nil)
129 if err != nil {212 if err != nil {
M internal/httpserve/middleware/requirelogin.go +74 -12
@@ -1,28 +1,90 @@
11 package middleware
22
33 import (
4+ "fmt"
45 "log"
56 "net/http"
7+ "time"
68
9+ "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/db/queries"
711 "github.com/labstack/echo-contrib/session"
812 "github.com/labstack/echo/v4"
913 )
1014
11-func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {
12- return func(c echo.Context) error {
13- sess, err := session.Get("session", c)
14- if err != nil {
15+func RequireLogin(a *app.App) echo.MiddlewareFunc {
16+ return func(next echo.HandlerFunc) echo.HandlerFunc {
17+ return func(c echo.Context) error {
18+ if err := validateSessionAndUser(c, a); err != nil {
19+ log.Println("Session validation failed:", err)
20+ return redirectToLogin(c, a)
21+ }
22+ return next(c)
23+ }
24+ }
25+}
1526
16- log.Println(err)
27+func validateSessionAndUser(c echo.Context, a *app.App) error {
28+ sess, err := session.Get("session", c)
29+ if err != nil {
30+ return err
31+ }
1732
18- return c.Redirect(http.StatusSeeOther, "/admin/login")
19- }
33+ if cookieExpired, err := isCookieExpired(c); err != nil || cookieExpired {
34+ return err
35+ }
2036
21- // Check if the user is authenticated
22- if sess.Values["authenticated"] == nil || sess.Values["authenticated"] == false {
23- return c.Redirect(http.StatusSeeOther, "/admin/login")
24- }
37+ accountID, ok := sess.Values["accountID"].(string)
38+ if !ok {
39+ return fmt.Errorf("invalid account ID type")
40+ }
41+
42+ sessionID, ok := sess.Values["sessionID"].(string)
43+ if !ok {
44+ return fmt.Errorf("invalid session ID type")
45+ }
2546
26- return next(c)
47+ if accountCheck, err := isAccountIDInDB(a, accountID); err != nil || !accountCheck {
48+ return err
2749 }
50+
51+ if sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID); err != nil || sessionExpired {
52+ return err
53+ }
54+
55+ return nil
56+}
57+
58+func redirectToLogin(c echo.Context, a *app.App) error {
59+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
60+}
61+
62+func isCookieExpired(c echo.Context) (bool, error) {
63+ cookie, err := c.Cookie("session")
64+ if err != nil {
65+ return false, err
66+ }
67+ if cookie.Expires.IsZero() {
68+ return false, nil // cookie has no expiration time; it's a session cookie
69+ }
70+ return time.Now().After(cookie.Expires), nil
71+}
72+
73+func isSessionExpiredInDB(a *app.App, userID string, sessionID string) (bool, error) {
74+ currentTime := time.Now()
75+ expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
76+ if err != nil {
77+ return false, err
78+ }
79+
80+ return currentTime.After(expirationTime), nil
81+}
82+
83+func isAccountIDInDB(a *app.App, accountID string) (bool, error) {
84+ accountExists, err := queries.CheckDBAccountExists(a, accountID)
85+ if err != nil {
86+ return false, err
87+ }
88+
89+ return accountExists, nil
2890 }
@@ -1,28 +1,90 @@
1 package middleware1 package middleware
2 2
3 import (3 import (
4+ "fmt"
4 "log"5 "log"
5 "net/http"6 "net/http"
7+ "time"
6 8
9+ "github.com/bnema/gordon/internal/app"
10+ "github.com/bnema/gordon/internal/db/queries"
7 "github.com/labstack/echo-contrib/session"11 "github.com/labstack/echo-contrib/session"
8 "github.com/labstack/echo/v4"12 "github.com/labstack/echo/v4"
9 )13 )
10 14
11-func RequireLogin(next echo.HandlerFunc) echo.HandlerFunc {15+func RequireLogin(a *app.App) echo.MiddlewareFunc {
12- return func(c echo.Context) error {16+ return func(next echo.HandlerFunc) echo.HandlerFunc {
13- sess, err := session.Get("session", c)17+ return func(c echo.Context) error {
14- if err != nil {18+ if err := validateSessionAndUser(c, a); err != nil {
19+ log.Println("Session validation failed:", err)
20+ return redirectToLogin(c, a)
21+ }
22+ return next(c)
23+ }
24+ }
25+}
15 26
16- log.Println(err)27+func validateSessionAndUser(c echo.Context, a *app.App) error {
28+ sess, err := session.Get("session", c)
29+ if err != nil {
30+ return err
31+ }
17 32
18- return c.Redirect(http.StatusSeeOther, "/admin/login")33+ if cookieExpired, err := isCookieExpired(c); err != nil || cookieExpired {
19- }34+ return err
35+ }
20 36
21- // Check if the user is authenticated37+ accountID, ok := sess.Values["accountID"].(string)
22- if sess.Values["authenticated"] == nil || sess.Values["authenticated"] == false {38+ if !ok {
23- return c.Redirect(http.StatusSeeOther, "/admin/login")39+ return fmt.Errorf("invalid account ID type")
24- }40+ }
41+
42+ sessionID, ok := sess.Values["sessionID"].(string)
43+ if !ok {
44+ return fmt.Errorf("invalid session ID type")
45+ }
25 46
26- return next(c)47+ if accountCheck, err := isAccountIDInDB(a, accountID); err != nil || !accountCheck {
48+ return err
27 }49 }
50+
51+ if sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID); err != nil || sessionExpired {
52+ return err
53+ }
54+
55+ return nil
56+}
57+
58+func redirectToLogin(c echo.Context, a *app.App) error {
59+ return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
60+}
61+
62+func isCookieExpired(c echo.Context) (bool, error) {
63+ cookie, err := c.Cookie("session")
64+ if err != nil {
65+ return false, err
66+ }
67+ if cookie.Expires.IsZero() {
68+ return false, nil // cookie has no expiration time; it's a session cookie
69+ }
70+ return time.Now().After(cookie.Expires), nil
71+}
72+
73+func isSessionExpiredInDB(a *app.App, userID string, sessionID string) (bool, error) {
74+ currentTime := time.Now()
75+ expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
76+ if err != nil {
77+ return false, err
78+ }
79+
80+ return currentTime.After(expirationTime), nil
81+}
82+
83+func isAccountIDInDB(a *app.App, accountID string) (bool, error) {
84+ accountExists, err := queries.CheckDBAccountExists(a, accountID)
85+ if err != nil {
86+ return false, err
87+ }
88+
89+ return accountExists, nil
28 }90 }
M internal/httpserve/middleware/secure.go +1 -1
@@ -30,7 +30,7 @@ func InitSessionMiddleware() echo.MiddlewareFunc {
3030 HttpOnly: true,
3131 Secure: true,
3232 SameSite: http.SameSiteStrictMode,
33- MaxAge: 86400 * 7,
33+ MaxAge: 86400,
3434 }
3535 return session.Middleware(store)
3636 }
@@ -30,7 +30,7 @@ func InitSessionMiddleware() echo.MiddlewareFunc {
30 HttpOnly: true,30 HttpOnly: true,
31 Secure: true,31 Secure: true,
32 SameSite: http.SameSiteStrictMode,32 SameSite: http.SameSiteStrictMode,
33- MaxAge: 86400 * 7,33+ MaxAge: 86400,
34 }34 }
35 return session.Middleware(store)35 return session.Middleware(store)
36 }36 }
M internal/httpserve/router.go +14 -12
@@ -45,17 +45,6 @@ func bindStaticRoute(e *echo.Echo, a *app.App, path string) {
4545 })
4646 }
4747
48-// bindAdminRoute binds all admin routes
49-func bindAdminRoute(e *echo.Echo, a *app.App, adminPath string) {
50- e.GET(adminPath, func(c echo.Context) error {
51- return handler.AdminRoute(c, a)
52- }, middleware.RequireLogin) // Require login
53-
54- e.GET(adminPath+"/manager", func(c echo.Context) error {
55- return handler.AdminManagerRoute(c, a)
56- })
57-}
58-
5948 // bindLoginRoute binds all login routes
6049 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
6150 e.GET(adminPath+"/login", func(c echo.Context) error {
@@ -72,13 +61,26 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
7261 })
7362 }
7463
64+// bindAdminRoute binds all admin routes
65+func bindAdminRoute(e *echo.Echo, a *app.App, adminPath string) {
66+ adminGroup := e.Group(adminPath)
67+ // Since login is behind the admin path, we cannot use group middleware
68+ adminGroup.GET("", func(c echo.Context) error {
69+ return c.Redirect(302, adminPath+"/manager")
70+ }, middleware.RequireLogin(a))
71+
72+ adminGroup.GET("/manager", func(c echo.Context) error {
73+ return handler.AdminManagerRoute(c, a)
74+ }, middleware.RequireLogin(a))
75+}
76+
7577 // bindHTMXEndpoints binds all HTMX endpoints
7678 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
7779 // Create a group for /htmx endpoints
7880 htmxGroup := e.Group("/htmx")
7981
8082 // Apply middleware to the group, so all endpoints are protected
81- htmxGroup.Use(middleware.RequireLogin)
83+ htmxGroup.Use(middleware.RequireLogin(a))
8284
8385 // List all images component
8486 htmxGroup.GET("/image-manager", func(c echo.Context) error {
@@ -45,17 +45,6 @@ func bindStaticRoute(e *echo.Echo, a *app.App, path string) {
45 })45 })
46 }46 }
47 47
48-// bindAdminRoute binds all admin routes
49-func bindAdminRoute(e *echo.Echo, a *app.App, adminPath string) {
50- e.GET(adminPath, func(c echo.Context) error {
51- return handler.AdminRoute(c, a)
52- }, middleware.RequireLogin) // Require login
53-
54- e.GET(adminPath+"/manager", func(c echo.Context) error {
55- return handler.AdminManagerRoute(c, a)
56- })
57-}
58-
59 // bindLoginRoute binds all login routes48 // bindLoginRoute binds all login routes
60 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {49 func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
61 e.GET(adminPath+"/login", func(c echo.Context) error {50 e.GET(adminPath+"/login", func(c echo.Context) error {
@@ -72,13 +61,26 @@ func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) {
72 })61 })
73 }62 }
74 63
64+// bindAdminRoute binds all admin routes
65+func bindAdminRoute(e *echo.Echo, a *app.App, adminPath string) {
66+ adminGroup := e.Group(adminPath)
67+ // Since login is behind the admin path, we cannot use group middleware
68+ adminGroup.GET("", func(c echo.Context) error {
69+ return c.Redirect(302, adminPath+"/manager")
70+ }, middleware.RequireLogin(a))
71+
72+ adminGroup.GET("/manager", func(c echo.Context) error {
73+ return handler.AdminManagerRoute(c, a)
74+ }, middleware.RequireLogin(a))
75+}
76+
75 // bindHTMXEndpoints binds all HTMX endpoints77 // bindHTMXEndpoints binds all HTMX endpoints
76 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {78 func bindHTMXEndpoints(e *echo.Echo, a *app.App) {
77 // Create a group for /htmx endpoints79 // Create a group for /htmx endpoints
78 htmxGroup := e.Group("/htmx")80 htmxGroup := e.Group("/htmx")
79 81
80 // Apply middleware to the group, so all endpoints are protected82 // Apply middleware to the group, so all endpoints are protected
81- htmxGroup.Use(middleware.RequireLogin)83+ htmxGroup.Use(middleware.RequireLogin(a))
82 84
83 // List all images component85 // List all images component
84 htmxGroup.GET("/image-manager", func(c echo.Context) error {86 htmxGroup.GET("/image-manager", func(c echo.Context) error {

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