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 ( | |||
| 23 | 23 | github.com/docker/go-units v0.5.0 // indirect | |
| 24 | 24 | github.com/gogo/protobuf v1.3.2 // indirect | |
| 25 | 25 | 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 | |
| 27 | 27 | github.com/gorilla/context v1.1.1 // indirect | |
| 28 | 28 | github.com/gorilla/css v1.0.0 // indirect | |
| 29 | 29 | github.com/gorilla/securecookie v1.1.1 // indirect | |
| @@ -23,7 +23,7 @@ require ( | |||||
| 23 | github.com/docker/go-units v0.5.0 // indirect | 23 | github.com/docker/go-units v0.5.0 // indirect | ||
| 24 | github.com/gogo/protobuf v1.3.2 // indirect | 24 | github.com/gogo/protobuf v1.3.2 // indirect | ||
| 25 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect | 25 | 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 |
| 27 | github.com/gorilla/context v1.1.1 // indirect | 27 | github.com/gorilla/context v1.1.1 // indirect | ||
| 28 | github.com/gorilla/css v1.0.0 // indirect | 28 | github.com/gorilla/css v1.0.0 // indirect | ||
| 29 | github.com/gorilla/securecookie v1.1.1 // indirect | 29 | github.com/gorilla/securecookie v1.1.1 // indirect | ||
M internal/app/sqliteinit.go +78 -0
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "os" | |
| 7 | 7 | "path/filepath" | |
| 8 | 8 | ||
| 9 | + | "github.com/bnema/gordon/internal/db" | |
| 9 | 10 | pkgsqlite "github.com/bnema/gordon/pkg/sqlite" | |
| 10 | 11 | _ "github.com/mattn/go-sqlite3" | |
| 11 | 12 | ) | |
| @@ -66,6 +67,13 @@ func InitializeDB(a *App) (*sql.DB, error) { | |||
| 66 | 67 | ||
| 67 | 68 | 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 | 77 | return db, nil | |
| 70 | 78 | } | |
| 71 | 79 | ||
| @@ -93,3 +101,73 @@ func bootstrapDB(dbPath string, app *App) error { | |||
| 93 | 101 | ||
| 94 | 102 | 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 | + | } | |
| @@ -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 = db | 68 | 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, nil | 77 | return db, nil | ||
| 70 | } | 78 | } | ||
| 71 | 79 | ||||
| @@ -93,3 +101,73 @@ func bootstrapDB(dbPath string, app *App) error { | |||||
| 93 | 101 | ||||
| 94 | return nil | 102 | 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 { | |||
| 18 | 18 | ID string `sql:"id, primary_key"` | |
| 19 | 19 | AccountID string `sql:"account_id, foreign_key=account.id"` | |
| 20 | 20 | BrowserInfo string `sql:"browser_info"` | |
| 21 | + | AccessToken string `sql:"access_token"` | |
| 21 | 22 | Expires string `sql:"expires"` | |
| 22 | 23 | IsOnline bool `sql:"is_online"` | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 25 | 26 | 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"` | |
| 34 | 34 | } | |
| @@ -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 ( | |||
| 12 | 12 | "github.com/bnema/gordon/internal/db" | |
| 13 | 13 | "github.com/bnema/gordon/internal/db/queries" | |
| 14 | 14 | "github.com/bnema/gordon/internal/templates/render" | |
| 15 | + | "github.com/gorilla/sessions" | |
| 15 | 16 | _ "github.com/joho/godotenv/autoload" | |
| 16 | 17 | "github.com/labstack/echo-contrib/session" | |
| 17 | 18 | "github.com/labstack/echo/v4" | |
| 18 | 19 | ) | |
| 19 | 20 | ||
| 21 | + | var urlToken string | |
| 22 | + | ||
| 20 | 23 | // compareGordonToken compares the token from the URL query parameter with the one from the config.yml | |
| 21 | 24 | func compareGordonToken(c echo.Context, a *app.App) error { | |
| 22 | - | urlToken := c.QueryParam("token") | |
| 23 | 25 | configToken := a.Config.General.GordonToken | |
| 24 | 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 | 30 | return nil | |
| 28 | 31 | } | |
| 29 | 32 | ||
| 30 | 33 | // RenderLoginPage renders the login.html template | |
| 31 | 34 | func RenderLoginPage(c echo.Context, a *app.App) error { | |
| 32 | - | ||
| 33 | - | // Compare the token | |
| 34 | - | compareGordonToken(c, a) | |
| 35 | - | ||
| 35 | + | urlToken = c.QueryParam("token") | |
| 36 | 36 | data := map[string]interface{}{ | |
| 37 | 37 | "Title": "Login", | |
| 38 | 38 | } | |
| @@ -58,6 +58,7 @@ func StartOAuthGithub(c echo.Context, a *app.App) error { | |||
| 58 | 58 | if err != nil { | |
| 59 | 59 | return err | |
| 60 | 60 | } | |
| 61 | + | ||
| 61 | 62 | //Initiate the Github OAuth flow | |
| 62 | 63 | clientID := os.Getenv("GITHUB_APP_ID") | |
| 63 | 64 | redirectDomain := a.Config.GenerateOauthCallbackURL() | |
| @@ -77,53 +78,135 @@ type Sessions struct { | |||
| 77 | 78 | } | |
| 78 | 79 | ||
| 79 | 80 | func OAuthCallback(c echo.Context, a *app.App) error { | |
| 81 | + | redirectPath := a.Config.Admin.Path | |
| 80 | 82 | ||
| 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) | |
| 86 | 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) | |
| 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") | |
| 93 | 96 | } | |
| 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 | |
| 100 | 101 | } | |
| 101 | 102 | ||
| 102 | 103 | browserInfo := c.Request().UserAgent() | |
| 103 | - | // print github user info | |
| 104 | 104 | userInfo, err := githubGetUserDetails(c, a) | |
| 105 | 105 | if err != nil { | |
| 106 | 106 | return err | |
| 107 | 107 | } | |
| 108 | 108 | ||
| 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) { | |
| 113 | 143 | userExists, err := queries.CheckDBUserExists(a) | |
| 114 | 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 | 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 | 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 | 210 | func fetchGithubAPI(url string, authHeader string, result interface{}) error { | |
| 128 | 211 | req, err := http.NewRequest("GET", url, nil) | |
| 129 | 212 | 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.yml | 23 | // 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.GordonToken | 25 | 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 nil | 30 | return nil | ||
| 28 | } | 31 | } | ||
| 29 | 32 | ||||
| 30 | // RenderLoginPage renders the login.html template | 33 | // 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 err | 59 | return err | ||
| 60 | } | 60 | } | ||
| 61 | + | ||||
| 61 | //Initiate the Github OAuth flow | 62 | //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 authenticated | 97 | + | |
| 95 | - | sess.Values["authenticated"] = true | 98 | + | sess, err := getSession(c) |
| 96 | - | sess.Values["access_token"] = accessToken | 99 | + | if err != nil { |
| 97 | - | sess.Values["expires"] = 604800 | 100 | + | 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 err | 106 | return err | ||
| 107 | } | 107 | } | ||
| 108 | 108 | ||||
| 109 | - | // Check if the user exists | 109 | + | user, err := handleUser(c, a, accessToken, browserInfo, userInfo) |
| 110 | - | // if not, create it | 110 | + | if err != nil { |
| 111 | - | // if yes, update the access token and create or update the session | 111 | + | return err |
| 112 | - | // If the user does not exist, create it | 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) { | |||
| 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 @@ | |||
| 1 | 1 | package middleware | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "log" | |
| 5 | 6 | "net/http" | |
| 7 | + | "time" | |
| 6 | 8 | ||
| 9 | + | "github.com/bnema/gordon/internal/app" | |
| 10 | + | "github.com/bnema/gordon/internal/db/queries" | |
| 7 | 11 | "github.com/labstack/echo-contrib/session" | |
| 8 | 12 | "github.com/labstack/echo/v4" | |
| 9 | 13 | ) | |
| 10 | 14 | ||
| 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 | + | } | |
| 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") | |
| 19 | - | } | |
| 33 | + | if cookieExpired, err := isCookieExpired(c); err != nil || cookieExpired { | |
| 34 | + | return err | |
| 35 | + | } | |
| 20 | 36 | ||
| 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 | + | } | |
| 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 | } | |
| @@ -1,28 +1,90 @@ | |||||
| 1 | package middleware | 1 | 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 authenticated | 37 | + | 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 { | |||
| 30 | 30 | HttpOnly: true, | |
| 31 | 31 | Secure: true, | |
| 32 | 32 | SameSite: http.SameSiteStrictMode, | |
| 33 | - | MaxAge: 86400 * 7, | |
| 33 | + | MaxAge: 86400, | |
| 34 | 34 | } | |
| 35 | 35 | return session.Middleware(store) | |
| 36 | 36 | } | |
| @@ -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) { | |||
| 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 | 48 | // bindLoginRoute binds all login routes | |
| 60 | 49 | func bindLoginRoute(e *echo.Echo, a *app.App, adminPath string) { | |
| 61 | 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 | 77 | // bindHTMXEndpoints binds all HTMX endpoints | |
| 76 | 78 | func bindHTMXEndpoints(e *echo.Echo, a *app.App) { | |
| 77 | 79 | // Create a group for /htmx endpoints | |
| 78 | 80 | htmxGroup := e.Group("/htmx") | |
| 79 | 81 | ||
| 80 | 82 | // Apply middleware to the group, so all endpoints are protected | |
| 81 | - | htmxGroup.Use(middleware.RequireLogin) | |
| 83 | + | htmxGroup.Use(middleware.RequireLogin(a)) | |
| 82 | 84 | ||
| 83 | 85 | // List all images component | |
| 84 | 86 | 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 routes | 48 | // 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 endpoints | 77 | // 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 endpoints | 79 | // 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 protected | 82 | // 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 component | 85 | // List all images component | ||
| 84 | htmxGroup.GET("/image-manager", func(c echo.Context) error { | 86 | htmxGroup.GET("/image-manager", func(c echo.Context) error { | ||