Improve Session Handling and various fixes
- Refactor session expiration and account validation logic - Remove redundant code for cookie and session expiration - Clarify comment and improve tag handling in push command - Start session cleaner cron job in server initialization - Add 'Clients' table in SQLite and to Account DB struct (for future use) - Remove old SessionCleaner function, add ExtendSessionExpiration - Implement custom filesystem to disable directory listing in static route - Modify RequireLogin middleware to delete cookie and redirect on failure
- commit
- 2b661a62fe2c557c8b95b69c39815dfc210fdaf4
- author
- bnema · · 2023-11-01 17:54 UTC
- parent
- 236dfe6
- signature
- unsigned
12 files · +127 -87
M internal/cli/cmd/push.go +4 -2
| @@ -42,7 +42,7 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 42 | 42 | return | |
| 43 | 43 | } | |
| 44 | 44 | ||
| 45 | - | // If there is :tag at the end of the image name, we append :latest | |
| 45 | + | // If there is no :tag at the end of the image name, we append :latest | |
| 46 | 46 | if !strings.Contains(imageName, ":") { | |
| 47 | 47 | imageName += ":latest" | |
| 48 | 48 | } | |
| @@ -54,19 +54,21 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||
| 54 | 54 | return | |
| 55 | 55 | } | |
| 56 | 56 | ||
| 57 | - | // Check the target domain struct http(s)://domain.tld (https is optional) | |
| 57 | + | // Check the target domain struct http(s)://domain.tld (proto is optional) | |
| 58 | 58 | match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain) | |
| 59 | 59 | if !match { | |
| 60 | 60 | fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used") | |
| 61 | 61 | return | |
| 62 | 62 | } | |
| 63 | 63 | ||
| 64 | + | // Get the image ID | |
| 64 | 65 | imageID, err := docker.GetImageID(imageName) | |
| 65 | 66 | if err != nil { | |
| 66 | 67 | fmt.Println("Error getting image ID:", err) | |
| 67 | 68 | return | |
| 68 | 69 | } | |
| 69 | 70 | ||
| 71 | + | // Get the image size | |
| 70 | 72 | totalSize, err := docker.GetImageSize(imageID) | |
| 71 | 73 | if err != nil { | |
| 72 | 74 | fmt.Println("Error estimating image size:", err) | |
| @@ -42,7 +42,7 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 42 | return | 42 | return | ||
| 43 | } | 43 | } | ||
| 44 | 44 | ||||
| 45 | - | // If there is :tag at the end of the image name, we append :latest | 45 | + | // If there is no :tag at the end of the image name, we append :latest |
| 46 | if !strings.Contains(imageName, ":") { | 46 | if !strings.Contains(imageName, ":") { | ||
| 47 | imageName += ":latest" | 47 | imageName += ":latest" | ||
| 48 | } | 48 | } | ||
| @@ -54,19 +54,21 @@ func NewPushCommand(a *cli.App) *cobra.Command { | |||||
| 54 | return | 54 | return | ||
| 55 | } | 55 | } | ||
| 56 | 56 | ||||
| 57 | - | // Check the target domain struct http(s)://domain.tld (https is optional) | 57 | + | // Check the target domain struct http(s)://domain.tld (proto is optional) |
| 58 | match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain) | 58 | match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain) | ||
| 59 | if !match { | 59 | if !match { | ||
| 60 | fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used") | 60 | fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used") | ||
| 61 | return | 61 | return | ||
| 62 | } | 62 | } | ||
| 63 | 63 | ||||
| 64 | + | // Get the image ID | |||
| 64 | imageID, err := docker.GetImageID(imageName) | 65 | imageID, err := docker.GetImageID(imageName) | ||
| 65 | if err != nil { | 66 | if err != nil { | ||
| 66 | fmt.Println("Error getting image ID:", err) | 67 | fmt.Println("Error getting image ID:", err) | ||
| 67 | return | 68 | return | ||
| 68 | } | 69 | } | ||
| 69 | 70 | ||||
| 71 | + | // Get the image size | |||
| 70 | totalSize, err := docker.GetImageSize(imageID) | 72 | totalSize, err := docker.GetImageSize(imageID) | ||
| 71 | if err != nil { | 73 | if err != nil { | ||
| 72 | fmt.Println("Error estimating image size:", err) | 74 | fmt.Println("Error estimating image size:", err) | ||
M internal/cli/handler/startserver.go +3 -0
| @@ -20,6 +20,9 @@ func StartServer(a *server.App, port string) error { | |||
| 20 | 20 | log.Fatal("Failed to initialize database:", err) | |
| 21 | 21 | } | |
| 22 | 22 | ||
| 23 | + | // Start the session cleaner cron job | |
| 24 | + | a.StartSessionCleaner() | |
| 25 | + | ||
| 23 | 26 | _, err = server.HandleNewTokenInitialization(a) | |
| 24 | 27 | if err != nil { | |
| 25 | 28 | log.Print(err) | |
| @@ -20,6 +20,9 @@ func StartServer(a *server.App, port string) error { | |||||
| 20 | log.Fatal("Failed to initialize database:", err) | 20 | log.Fatal("Failed to initialize database:", err) | ||
| 21 | } | 21 | } | ||
| 22 | 22 | ||||
| 23 | + | // Start the session cleaner cron job | |||
| 24 | + | a.StartSessionCleaner() | |||
| 25 | + | ||||
| 23 | _, err = server.HandleNewTokenInitialization(a) | 26 | _, err = server.HandleNewTokenInitialization(a) | ||
| 24 | if err != nil { | 27 | if err != nil { | ||
| 25 | log.Print(err) | 28 | log.Print(err) | ||
M internal/db/dbstruct.go +10 -0
| @@ -12,6 +12,7 @@ type Account struct { | |||
| 12 | 12 | UserID string `sql:"user_id, foreign_key=user.id"` | |
| 13 | 13 | Sessions []Sessions | |
| 14 | 14 | Providers []Provider | |
| 15 | + | Clients []Clients | |
| 15 | 16 | } | |
| 16 | 17 | ||
| 17 | 18 | type Sessions struct { | |
| @@ -32,3 +33,12 @@ type Provider struct { | |||
| 32 | 33 | ProfileURL string `sql:"profile_url"` | |
| 33 | 34 | Email string `sql:"email"` | |
| 34 | 35 | } | |
| 36 | + | ||
| 37 | + | type Clients struct { | |
| 38 | + | ID string `sql:"id, primary_key"` | |
| 39 | + | AccountID string `sql:"account_id, foreign_key=account.id"` | |
| 40 | + | OS string `sql:"os"` | |
| 41 | + | IP string `sql:"ip"` | |
| 42 | + | Hostname string `sql:"hostname"` | |
| 43 | + | Expires string `sql:"expires"` | |
| 44 | + | } | |
| @@ -12,6 +12,7 @@ type Account struct { | |||||
| 12 | UserID string `sql:"user_id, foreign_key=user.id"` | 12 | UserID string `sql:"user_id, foreign_key=user.id"` | ||
| 13 | Sessions []Sessions | 13 | Sessions []Sessions | ||
| 14 | Providers []Provider | 14 | Providers []Provider | ||
| 15 | + | Clients []Clients | |||
| 15 | } | 16 | } | ||
| 16 | 17 | ||||
| 17 | type Sessions struct { | 18 | type Sessions struct { | ||
| @@ -32,3 +33,12 @@ type Provider struct { | |||||
| 32 | ProfileURL string `sql:"profile_url"` | 33 | ProfileURL string `sql:"profile_url"` | ||
| 33 | Email string `sql:"email"` | 34 | Email string `sql:"email"` | ||
| 34 | } | 35 | } | ||
| 36 | + | ||||
| 37 | + | type Clients struct { | |||
| 38 | + | ID string `sql:"id, primary_key"` | |||
| 39 | + | AccountID string `sql:"account_id, foreign_key=account.id"` | |||
| 40 | + | OS string `sql:"os"` | |||
| 41 | + | IP string `sql:"ip"` | |||
| 42 | + | Hostname string `sql:"hostname"` | |||
| 43 | + | Expires string `sql:"expires"` | |||
| 44 | + | } | |||
A internal/db/queries/clients.go +1 -0
| @@ -0,0 +1,1 @@ | |||
| 1 | + | package queries | |
| @@ -0,0 +1,1 @@ | |||||
| 1 | + | package queries | |||
M internal/db/queries/sessions.go +10 -11
| @@ -82,17 +82,6 @@ func CreateOrUpdateDBSession(a *server.App, accessToken string, browserInfo stri | |||
| 82 | 82 | return nil | |
| 83 | 83 | } | |
| 84 | 84 | ||
| 85 | - | // SessionCleaner, time as a string | |
| 86 | - | func SessionCleaner(a *server.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 | 85 | // DeleteSession deletes the session from the database. | |
| 97 | 86 | func DeleteDBSession(a *server.App, accountID string, sessionID string) error { | |
| 98 | 87 | query := "DELETE FROM sessions WHERE account_id = ? AND id = ?" | |
| @@ -121,3 +110,13 @@ func GetSessionExpiration(a *server.App, accountID string, sessionID string, cur | |||
| 121 | 110 | ||
| 122 | 111 | return expirationTimeParsed, nil | |
| 123 | 112 | } | |
| 113 | + | ||
| 114 | + | func ExtendSessionExpiration(a *server.App, accountID string, sessionID string, newExpirationTime time.Time) error { | |
| 115 | + | query := "UPDATE sessions SET expires = ? WHERE account_id = ? AND id = ?" | |
| 116 | + | _, err := a.DB.Exec(query, newExpirationTime.Format(time.RFC3339), accountID, sessionID) | |
| 117 | + | if err != nil { | |
| 118 | + | return err | |
| 119 | + | } | |
| 120 | + | ||
| 121 | + | return nil | |
| 122 | + | } | |
| @@ -82,17 +82,6 @@ func CreateOrUpdateDBSession(a *server.App, accessToken string, browserInfo stri | |||||
| 82 | return nil | 82 | return nil | ||
| 83 | } | 83 | } | ||
| 84 | 84 | ||||
| 85 | - | // SessionCleaner, time as a string | |||
| 86 | - | func SessionCleaner(a *server.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 | // DeleteSession deletes the session from the database. | 85 | // DeleteSession deletes the session from the database. | ||
| 97 | func DeleteDBSession(a *server.App, accountID string, sessionID string) error { | 86 | func DeleteDBSession(a *server.App, accountID string, sessionID string) error { | ||
| 98 | query := "DELETE FROM sessions WHERE account_id = ? AND id = ?" | 87 | query := "DELETE FROM sessions WHERE account_id = ? AND id = ?" | ||
| @@ -121,3 +110,13 @@ func GetSessionExpiration(a *server.App, accountID string, sessionID string, cur | |||||
| 121 | 110 | ||||
| 122 | return expirationTimeParsed, nil | 111 | return expirationTimeParsed, nil | ||
| 123 | } | 112 | } | ||
| 113 | + | ||||
| 114 | + | func ExtendSessionExpiration(a *server.App, accountID string, sessionID string, newExpirationTime time.Time) error { | |||
| 115 | + | query := "UPDATE sessions SET expires = ? WHERE account_id = ? AND id = ?" | |||
| 116 | + | _, err := a.DB.Exec(query, newExpirationTime.Format(time.RFC3339), accountID, sessionID) | |||
| 117 | + | if err != nil { | |||
| 118 | + | return err | |||
| 119 | + | } | |||
| 120 | + | ||||
| 121 | + | return nil | |||
| 122 | + | } | |||
M internal/httpserve/handler/static.go +35 -4
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package handler | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "io/fs" | |
| 4 | 5 | "net/http" | |
| 5 | 6 | "os" | |
| 6 | 7 | ||
| @@ -8,17 +9,47 @@ import ( | |||
| 8 | 9 | "github.com/labstack/echo/v4" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 12 | + | // NoDirFileSys restricts directory listing | |
| 13 | + | type NoDirFileSys struct { | |
| 14 | + | fs http.FileSystem | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func (nfs NoDirFileSys) Open(name string) (http.File, error) { | |
| 18 | + | f, err := nfs.fs.Open(name) | |
| 19 | + | if err != nil { | |
| 20 | + | return nil, err | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | return NoDirFile{f}, nil | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // NoDirFile restricts directory listing | |
| 27 | + | type NoDirFile struct { | |
| 28 | + | http.File | |
| 29 | + | } | |
| 30 | + | ||
| 31 | + | func (f NoDirFile) Readdir(count int) ([]fs.FileInfo, error) { | |
| 32 | + | // Disable directory listing | |
| 33 | + | return nil, nil | |
| 34 | + | } | |
| 35 | + | ||
| 11 | 36 | // StaticRoute serves static files from the embedded filesystem | |
| 12 | 37 | func StaticRoute(c echo.Context, a *server.App) error { | |
| 13 | - | // Set the cache-control header based on PROD environment variable | |
| 14 | - | if os.Getenv("PROD") == "true" { | |
| 38 | + | // Set the cache-control header if we are not in dev mode | |
| 39 | + | if os.Getenv("RUN_ENV") != "dev" { | |
| 15 | 40 | c.Response().Header().Set("Cache-Control", "public, max-age=86400") | |
| 16 | 41 | } else { | |
| 17 | 42 | c.Response().Header().Set("Cache-Control", "no-cache") | |
| 18 | 43 | } | |
| 19 | 44 | ||
| 20 | - | // Serve the file from the embedded filesystem | |
| 21 | - | publicFS := http.FileServer(http.FS(a.PublicFS)) | |
| 45 | + | // Convert fs.FS to http.FileSystem | |
| 46 | + | httpFS := http.FS(a.PublicFS) | |
| 47 | + | ||
| 48 | + | // Use custom file system wrapper to disable directory listing | |
| 49 | + | noDirFS := NoDirFileSys{httpFS} | |
| 50 | + | ||
| 51 | + | // Serve the file from the custom filesystem | |
| 52 | + | publicFS := http.FileServer(noDirFS) | |
| 22 | 53 | publicFS.ServeHTTP(c.Response(), c.Request()) | |
| 23 | 54 | return nil | |
| 24 | 55 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package handler | 1 | package handler | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "io/fs" | |||
| 4 | "net/http" | 5 | "net/http" | ||
| 5 | "os" | 6 | "os" | ||
| 6 | 7 | ||||
| @@ -8,17 +9,47 @@ import ( | |||||
| 8 | "github.com/labstack/echo/v4" | 9 | "github.com/labstack/echo/v4" | ||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 12 | + | // NoDirFileSys restricts directory listing | |||
| 13 | + | type NoDirFileSys struct { | |||
| 14 | + | fs http.FileSystem | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func (nfs NoDirFileSys) Open(name string) (http.File, error) { | |||
| 18 | + | f, err := nfs.fs.Open(name) | |||
| 19 | + | if err != nil { | |||
| 20 | + | return nil, err | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | return NoDirFile{f}, nil | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // NoDirFile restricts directory listing | |||
| 27 | + | type NoDirFile struct { | |||
| 28 | + | http.File | |||
| 29 | + | } | |||
| 30 | + | ||||
| 31 | + | func (f NoDirFile) Readdir(count int) ([]fs.FileInfo, error) { | |||
| 32 | + | // Disable directory listing | |||
| 33 | + | return nil, nil | |||
| 34 | + | } | |||
| 35 | + | ||||
| 11 | // StaticRoute serves static files from the embedded filesystem | 36 | // StaticRoute serves static files from the embedded filesystem | ||
| 12 | func StaticRoute(c echo.Context, a *server.App) error { | 37 | func StaticRoute(c echo.Context, a *server.App) error { | ||
| 13 | - | // Set the cache-control header based on PROD environment variable | 38 | + | // Set the cache-control header if we are not in dev mode |
| 14 | - | if os.Getenv("PROD") == "true" { | 39 | + | if os.Getenv("RUN_ENV") != "dev" { |
| 15 | c.Response().Header().Set("Cache-Control", "public, max-age=86400") | 40 | c.Response().Header().Set("Cache-Control", "public, max-age=86400") | ||
| 16 | } else { | 41 | } else { | ||
| 17 | c.Response().Header().Set("Cache-Control", "no-cache") | 42 | c.Response().Header().Set("Cache-Control", "no-cache") | ||
| 18 | } | 43 | } | ||
| 19 | 44 | ||||
| 20 | - | // Serve the file from the embedded filesystem | 45 | + | // Convert fs.FS to http.FileSystem |
| 21 | - | publicFS := http.FileServer(http.FS(a.PublicFS)) | 46 | + | httpFS := http.FS(a.PublicFS) |
| 47 | + | ||||
| 48 | + | // Use custom file system wrapper to disable directory listing | |||
| 49 | + | noDirFS := NoDirFileSys{httpFS} | |||
| 50 | + | ||||
| 51 | + | // Serve the file from the custom filesystem | |||
| 52 | + | publicFS := http.FileServer(noDirFS) | |||
| 22 | publicFS.ServeHTTP(c.Response(), c.Request()) | 53 | publicFS.ServeHTTP(c.Response(), c.Request()) | ||
| 23 | return nil | 54 | return nil | ||
| 24 | } | 55 | } | ||
M internal/httpserve/middleware/requirelogin.go +16 -63
| @@ -18,7 +18,7 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc { | |||
| 18 | 18 | return func(c echo.Context) error { | |
| 19 | 19 | if err := validateSessionAndUser(c, a); err != nil { | |
| 20 | 20 | log.Println("Session validation failed:", err) | |
| 21 | - | return redirectToLogin(c, a) | |
| 21 | + | return DeleteCookieAndRedirectToLogin(c, a) | |
| 22 | 22 | } | |
| 23 | 23 | return next(c) | |
| 24 | 24 | } | |
| @@ -26,52 +26,37 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc { | |||
| 26 | 26 | } | |
| 27 | 27 | ||
| 28 | 28 | func validateSessionAndUser(c echo.Context, a *server.App) error { | |
| 29 | + | sessionExpiryTime := 30 * time.Minute | |
| 30 | + | ||
| 29 | 31 | sess, err := session.Get("session", c) | |
| 30 | 32 | if err != nil { | |
| 31 | 33 | return err | |
| 32 | 34 | } | |
| 33 | 35 | ||
| 34 | - | // Retrieve accountID and sessionID from session values | |
| 35 | 36 | accountID, ok := sess.Values["accountID"].(string) | |
| 36 | 37 | if !ok { | |
| 37 | - | return fmt.Errorf("invalid account ID type") | |
| 38 | + | return fmt.Errorf("accountID not found or invalid type") | |
| 38 | 39 | } | |
| 39 | 40 | ||
| 40 | 41 | sessionID, ok := sess.Values["sessionID"].(string) | |
| 41 | 42 | if !ok { | |
| 42 | - | return fmt.Errorf("invalid session ID type") | |
| 43 | - | } | |
| 44 | - | ||
| 45 | - | // Retrieve expires from session values and parse it to time.Time | |
| 46 | - | expiresStr, ok := sess.Values["expires"].(string) | |
| 47 | - | if !ok { | |
| 48 | - | return fmt.Errorf("invalid expires type") | |
| 49 | - | } | |
| 50 | - | expires, err := time.Parse(time.RFC3339, expiresStr) | |
| 51 | - | if err != nil { | |
| 52 | - | return fmt.Errorf("could not parse expires: %v", err) | |
| 43 | + | return fmt.Errorf("sessionID not found or invalid type") | |
| 53 | 44 | } | |
| 54 | 45 | ||
| 55 | - | if accountCheck, err := isAccountIDInDB(a, accountID); err != nil || !accountCheck { | |
| 56 | - | fmt.Println("accountCheck:", accountCheck) | |
| 57 | - | return err | |
| 46 | + | accountExists, err := isAccountIDInDB(a, accountID) | |
| 47 | + | if err != nil || !accountExists { | |
| 48 | + | return fmt.Errorf("account does not exist") | |
| 58 | 49 | } | |
| 59 | 50 | ||
| 60 | - | if sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID); err != nil || sessionExpired { | |
| 61 | - | fmt.Println("sessionExpired:", sessionExpired) | |
| 62 | - | return err | |
| 51 | + | sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID) | |
| 52 | + | if err != nil || sessionExpired { | |
| 53 | + | return fmt.Errorf("session is expired") | |
| 63 | 54 | } | |
| 64 | 55 | ||
| 65 | - | if cookieExpired, err := isCookieExpired(c); err != nil || cookieExpired { | |
| 66 | - | fmt.Println("cookieExpired:", cookieExpired) | |
| 67 | - | return err | |
| 68 | - | } | |
| 69 | - | ||
| 70 | - | if time.Now().After(expires) { | |
| 71 | - | fmt.Println("session expired") | |
| 72 | - | queries.SessionCleaner(a, time.Now().Format(time.RFC3339)) | |
| 73 | - | return fmt.Errorf("session expired") | |
| 74 | - | ||
| 56 | + | // Extend session expiration conditionally, e.g., if it's close to expiring | |
| 57 | + | err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime)) | |
| 58 | + | if err != nil { | |
| 59 | + | return fmt.Errorf("failed to extend session: %v", err) | |
| 75 | 60 | } | |
| 76 | 61 | ||
| 77 | 62 | return nil | |
| @@ -81,17 +66,6 @@ func redirectToLogin(c echo.Context, a *server.App) error { | |||
| 81 | 66 | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login") | |
| 82 | 67 | } | |
| 83 | 68 | ||
| 84 | - | func isCookieExpired(c echo.Context) (bool, error) { | |
| 85 | - | cookie, err := c.Cookie("session") | |
| 86 | - | if err != nil { | |
| 87 | - | return false, err | |
| 88 | - | } | |
| 89 | - | ||
| 90 | - | currentTime := time.Now() | |
| 91 | - | expirationTime := cookie.Expires | |
| 92 | - | return currentTime.After(expirationTime), nil | |
| 93 | - | } | |
| 94 | - | ||
| 95 | 69 | func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) { | |
| 96 | 70 | currentTime := time.Now() | |
| 97 | 71 | expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime) | |
| @@ -111,28 +85,7 @@ func isAccountIDInDB(a *server.App, accountID string) (bool, error) { | |||
| 111 | 85 | return accountExists, nil | |
| 112 | 86 | } | |
| 113 | 87 | ||
| 114 | - | func DeleteAndRedirectToLogin(c echo.Context, a *server.App) error { | |
| 115 | - | sess, err := session.Get("session", c) | |
| 116 | - | if err != nil { | |
| 117 | - | return err | |
| 118 | - | } | |
| 119 | - | ||
| 120 | - | // Retrieve accountID and sessionID from session values | |
| 121 | - | accountID, ok := sess.Values["accountID"].(string) | |
| 122 | - | if !ok { | |
| 123 | - | return fmt.Errorf("invalid account ID type") | |
| 124 | - | } | |
| 125 | - | ||
| 126 | - | sessionID, ok := sess.Values["sessionID"].(string) | |
| 127 | - | if !ok { | |
| 128 | - | return fmt.Errorf("invalid session ID type") | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | // Delete session from DB | |
| 132 | - | if err := queries.DeleteDBSession(a, accountID, sessionID); err != nil { | |
| 133 | - | return err | |
| 134 | - | } | |
| 135 | - | ||
| 88 | + | func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error { | |
| 136 | 89 | // Delete session cookie | |
| 137 | 90 | cookie := new(http.Cookie) | |
| 138 | 91 | cookie.Name = "session" | |
| @@ -18,7 +18,7 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc { | |||||
| 18 | return func(c echo.Context) error { | 18 | return func(c echo.Context) error { | ||
| 19 | if err := validateSessionAndUser(c, a); err != nil { | 19 | if err := validateSessionAndUser(c, a); err != nil { | ||
| 20 | log.Println("Session validation failed:", err) | 20 | log.Println("Session validation failed:", err) | ||
| 21 | - | return redirectToLogin(c, a) | 21 | + | return DeleteCookieAndRedirectToLogin(c, a) |
| 22 | } | 22 | } | ||
| 23 | return next(c) | 23 | return next(c) | ||
| 24 | } | 24 | } | ||
| @@ -26,52 +26,37 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc { | |||||
| 26 | } | 26 | } | ||
| 27 | 27 | ||||
| 28 | func validateSessionAndUser(c echo.Context, a *server.App) error { | 28 | func validateSessionAndUser(c echo.Context, a *server.App) error { | ||
| 29 | + | sessionExpiryTime := 30 * time.Minute | |||
| 30 | + | ||||
| 29 | sess, err := session.Get("session", c) | 31 | sess, err := session.Get("session", c) | ||
| 30 | if err != nil { | 32 | if err != nil { | ||
| 31 | return err | 33 | return err | ||
| 32 | } | 34 | } | ||
| 33 | 35 | ||||
| 34 | - | // Retrieve accountID and sessionID from session values | |||
| 35 | accountID, ok := sess.Values["accountID"].(string) | 36 | accountID, ok := sess.Values["accountID"].(string) | ||
| 36 | if !ok { | 37 | if !ok { | ||
| 37 | - | return fmt.Errorf("invalid account ID type") | 38 | + | return fmt.Errorf("accountID not found or invalid type") |
| 38 | } | 39 | } | ||
| 39 | 40 | ||||
| 40 | sessionID, ok := sess.Values["sessionID"].(string) | 41 | sessionID, ok := sess.Values["sessionID"].(string) | ||
| 41 | if !ok { | 42 | if !ok { | ||
| 42 | - | return fmt.Errorf("invalid session ID type") | 43 | + | return fmt.Errorf("sessionID not found or invalid type") |
| 43 | - | } | |||
| 44 | - | ||||
| 45 | - | // Retrieve expires from session values and parse it to time.Time | |||
| 46 | - | expiresStr, ok := sess.Values["expires"].(string) | |||
| 47 | - | if !ok { | |||
| 48 | - | return fmt.Errorf("invalid expires type") | |||
| 49 | - | } | |||
| 50 | - | expires, err := time.Parse(time.RFC3339, expiresStr) | |||
| 51 | - | if err != nil { | |||
| 52 | - | return fmt.Errorf("could not parse expires: %v", err) | |||
| 53 | } | 44 | } | ||
| 54 | 45 | ||||
| 55 | - | if accountCheck, err := isAccountIDInDB(a, accountID); err != nil || !accountCheck { | 46 | + | accountExists, err := isAccountIDInDB(a, accountID) |
| 56 | - | fmt.Println("accountCheck:", accountCheck) | 47 | + | if err != nil || !accountExists { |
| 57 | - | return err | 48 | + | return fmt.Errorf("account does not exist") |
| 58 | } | 49 | } | ||
| 59 | 50 | ||||
| 60 | - | if sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID); err != nil || sessionExpired { | 51 | + | sessionExpired, err := isSessionExpiredInDB(a, accountID, sessionID) |
| 61 | - | fmt.Println("sessionExpired:", sessionExpired) | 52 | + | if err != nil || sessionExpired { |
| 62 | - | return err | 53 | + | return fmt.Errorf("session is expired") |
| 63 | } | 54 | } | ||
| 64 | 55 | ||||
| 65 | - | if cookieExpired, err := isCookieExpired(c); err != nil || cookieExpired { | 56 | + | // Extend session expiration conditionally, e.g., if it's close to expiring |
| 66 | - | fmt.Println("cookieExpired:", cookieExpired) | 57 | + | err = queries.ExtendSessionExpiration(a, accountID, sessionID, time.Now().Add(sessionExpiryTime)) |
| 67 | - | return err | 58 | + | if err != nil { |
| 68 | - | } | 59 | + | return fmt.Errorf("failed to extend session: %v", err) |
| 69 | - | ||||
| 70 | - | if time.Now().After(expires) { | |||
| 71 | - | fmt.Println("session expired") | |||
| 72 | - | queries.SessionCleaner(a, time.Now().Format(time.RFC3339)) | |||
| 73 | - | return fmt.Errorf("session expired") | |||
| 74 | - | ||||
| 75 | } | 60 | } | ||
| 76 | 61 | ||||
| 77 | return nil | 62 | return nil | ||
| @@ -81,17 +66,6 @@ func redirectToLogin(c echo.Context, a *server.App) error { | |||||
| 81 | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login") | 66 | return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login") | ||
| 82 | } | 67 | } | ||
| 83 | 68 | ||||
| 84 | - | func isCookieExpired(c echo.Context) (bool, error) { | |||
| 85 | - | cookie, err := c.Cookie("session") | |||
| 86 | - | if err != nil { | |||
| 87 | - | return false, err | |||
| 88 | - | } | |||
| 89 | - | ||||
| 90 | - | currentTime := time.Now() | |||
| 91 | - | expirationTime := cookie.Expires | |||
| 92 | - | return currentTime.After(expirationTime), nil | |||
| 93 | - | } | |||
| 94 | - | ||||
| 95 | func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) { | 69 | func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) { | ||
| 96 | currentTime := time.Now() | 70 | currentTime := time.Now() | ||
| 97 | expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime) | 71 | expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime) | ||
| @@ -111,28 +85,7 @@ func isAccountIDInDB(a *server.App, accountID string) (bool, error) { | |||||
| 111 | return accountExists, nil | 85 | return accountExists, nil | ||
| 112 | } | 86 | } | ||
| 113 | 87 | ||||
| 114 | - | func DeleteAndRedirectToLogin(c echo.Context, a *server.App) error { | 88 | + | func DeleteCookieAndRedirectToLogin(c echo.Context, a *server.App) error { |
| 115 | - | sess, err := session.Get("session", c) | |||
| 116 | - | if err != nil { | |||
| 117 | - | return err | |||
| 118 | - | } | |||
| 119 | - | ||||
| 120 | - | // Retrieve accountID and sessionID from session values | |||
| 121 | - | accountID, ok := sess.Values["accountID"].(string) | |||
| 122 | - | if !ok { | |||
| 123 | - | return fmt.Errorf("invalid account ID type") | |||
| 124 | - | } | |||
| 125 | - | ||||
| 126 | - | sessionID, ok := sess.Values["sessionID"].(string) | |||
| 127 | - | if !ok { | |||
| 128 | - | return fmt.Errorf("invalid session ID type") | |||
| 129 | - | } | |||
| 130 | - | ||||
| 131 | - | // Delete session from DB | |||
| 132 | - | if err := queries.DeleteDBSession(a, accountID, sessionID); err != nil { | |||
| 133 | - | return err | |||
| 134 | - | } | |||
| 135 | - | ||||
| 136 | // Delete session cookie | 89 | // Delete session cookie | ||
| 137 | cookie := new(http.Cookie) | 90 | cookie := new(http.Cookie) | ||
| 138 | cookie.Name = "session" | 91 | cookie.Name = "session" | ||
M internal/httpserve/router.go +3 -5
| @@ -10,19 +10,18 @@ import ( | |||
| 10 | 10 | ||
| 11 | 11 | // RegisterRoutes registers all routes for the application | |
| 12 | 12 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |
| 13 | - | // --- Register API endpoints --- // | |
| 14 | - | bindAPIEndpoints(e, a) | |
| 15 | 13 | AdminPath := a.Config.Admin.Path | |
| 14 | + | bindLoginRoute(e, a, AdminPath) | |
| 15 | + | bindAPIEndpoints(e, a) | |
| 16 | + | ||
| 16 | 17 | // SetCommonDataMiddleware will pass data to the renderer | |
| 17 | 18 | e.Use(middleware.SetCommonDataMiddleware(a)) | |
| 18 | 19 | // Error handler middleware | |
| 19 | 20 | e.Use(middleware.ErrorHandler) | |
| 20 | - | ||
| 21 | 21 | // Initiate the session middleware | |
| 22 | 22 | e.Use(middleware.InitSessionMiddleware(a)) | |
| 23 | 23 | // Use middlewares | |
| 24 | 24 | e.Use(middleware.SecureRoutes()) | |
| 25 | - | ||
| 26 | 25 | // Color scheme detection for dark/light mode | |
| 27 | 26 | e.Use(middleware.ColorSchemeDetection) | |
| 28 | 27 | // Language detection | |
| @@ -31,7 +30,6 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||
| 31 | 30 | // --- Register routes --- // | |
| 32 | 31 | bindAdminRoute(e, a, AdminPath) | |
| 33 | 32 | bindStaticRoute(e, a, "/*") | |
| 34 | - | bindLoginRoute(e, a, AdminPath) | |
| 35 | 33 | bindHTMXEndpoints(e, a) | |
| 36 | 34 | // Protect the root path with a 403 | |
| 37 | 35 | e.GET("/", func(c echo.Context) error { | |
| @@ -10,19 +10,18 @@ import ( | |||||
| 10 | 10 | ||||
| 11 | // RegisterRoutes registers all routes for the application | 11 | // RegisterRoutes registers all routes for the application | ||
| 12 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | 12 | func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | ||
| 13 | - | // --- Register API endpoints --- // | |||
| 14 | - | bindAPIEndpoints(e, a) | |||
| 15 | AdminPath := a.Config.Admin.Path | 13 | AdminPath := a.Config.Admin.Path | ||
| 14 | + | bindLoginRoute(e, a, AdminPath) | |||
| 15 | + | bindAPIEndpoints(e, a) | |||
| 16 | + | ||||
| 16 | // SetCommonDataMiddleware will pass data to the renderer | 17 | // SetCommonDataMiddleware will pass data to the renderer | ||
| 17 | e.Use(middleware.SetCommonDataMiddleware(a)) | 18 | e.Use(middleware.SetCommonDataMiddleware(a)) | ||
| 18 | // Error handler middleware | 19 | // Error handler middleware | ||
| 19 | e.Use(middleware.ErrorHandler) | 20 | e.Use(middleware.ErrorHandler) | ||
| 20 | - | ||||
| 21 | // Initiate the session middleware | 21 | // Initiate the session middleware | ||
| 22 | e.Use(middleware.InitSessionMiddleware(a)) | 22 | e.Use(middleware.InitSessionMiddleware(a)) | ||
| 23 | // Use middlewares | 23 | // Use middlewares | ||
| 24 | e.Use(middleware.SecureRoutes()) | 24 | e.Use(middleware.SecureRoutes()) | ||
| 25 | - | ||||
| 26 | // Color scheme detection for dark/light mode | 25 | // Color scheme detection for dark/light mode | ||
| 27 | e.Use(middleware.ColorSchemeDetection) | 26 | e.Use(middleware.ColorSchemeDetection) | ||
| 28 | // Language detection | 27 | // Language detection | ||
| @@ -31,7 +30,6 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo { | |||||
| 31 | // --- Register routes --- // | 30 | // --- Register routes --- // | ||
| 32 | bindAdminRoute(e, a, AdminPath) | 31 | bindAdminRoute(e, a, AdminPath) | ||
| 33 | bindStaticRoute(e, a, "/*") | 32 | bindStaticRoute(e, a, "/*") | ||
| 34 | - | bindLoginRoute(e, a, AdminPath) | |||
| 35 | bindHTMXEndpoints(e, a) | 33 | bindHTMXEndpoints(e, a) | ||
| 36 | // Protect the root path with a 403 | 34 | // Protect the root path with a 403 | ||
| 37 | e.GET("/", func(c echo.Context) error { | 35 | e.GET("/", func(c echo.Context) error { | ||
M internal/server/appserver.go +1 -0
| @@ -30,6 +30,7 @@ type DBTables struct { | |||
| 30 | 30 | Account db.Account `sql:"account"` | |
| 31 | 31 | Sessions db.Sessions `sql:"sessions"` | |
| 32 | 32 | Provider db.Provider `sql:"provider"` | |
| 33 | + | Clients db.Clients `sql:"clients"` | |
| 33 | 34 | } | |
| 34 | 35 | ||
| 35 | 36 | // GenerateOauthCallbackURL generates the OAuth callback URL | |
| @@ -30,6 +30,7 @@ type DBTables struct { | |||||
| 30 | Account db.Account `sql:"account"` | 30 | Account db.Account `sql:"account"` | ||
| 31 | Sessions db.Sessions `sql:"sessions"` | 31 | Sessions db.Sessions `sql:"sessions"` | ||
| 32 | Provider db.Provider `sql:"provider"` | 32 | Provider db.Provider `sql:"provider"` | ||
| 33 | + | Clients db.Clients `sql:"clients"` | |||
| 33 | } | 34 | } | ||
| 34 | 35 | ||||
| 35 | // GenerateOauthCallbackURL generates the OAuth callback URL | 36 | // GenerateOauthCallbackURL generates the OAuth callback URL | ||
A internal/server/cronjobs.go +30 -0
| @@ -0,0 +1,30 @@ | |||
| 1 | + | package server | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "log" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // SessionCleaner, time as a string | |
| 9 | + | func SessionCleaner(a *App, currentTime string) error { | |
| 10 | + | query := "DELETE FROM sessions WHERE expires < ?" | |
| 11 | + | _, err := a.DB.Exec(query, currentTime) | |
| 12 | + | if err != nil { | |
| 13 | + | return err | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | return nil | |
| 17 | + | } | |
| 18 | + | ||
| 19 | + | // StartSessionCleaner initializes the session cleaner to run at regular intervals. | |
| 20 | + | func (a *App) StartSessionCleaner() { | |
| 21 | + | go func() { | |
| 22 | + | for { | |
| 23 | + | currentTime := time.Now().Format(time.RFC3339) | |
| 24 | + | if err := SessionCleaner(a, currentTime); err != nil { | |
| 25 | + | log.Printf("Failed to clean up sessions: %v", err) | |
| 26 | + | } | |
| 27 | + | time.Sleep(1 * time.Hour) | |
| 28 | + | } | |
| 29 | + | }() | |
| 30 | + | } | |
| @@ -0,0 +1,30 @@ | |||||
| 1 | + | package server | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "log" | |||
| 5 | + | "time" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // SessionCleaner, time as a string | |||
| 9 | + | func SessionCleaner(a *App, currentTime string) error { | |||
| 10 | + | query := "DELETE FROM sessions WHERE expires < ?" | |||
| 11 | + | _, err := a.DB.Exec(query, currentTime) | |||
| 12 | + | if err != nil { | |||
| 13 | + | return err | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | return nil | |||
| 17 | + | } | |||
| 18 | + | ||||
| 19 | + | // StartSessionCleaner initializes the session cleaner to run at regular intervals. | |||
| 20 | + | func (a *App) StartSessionCleaner() { | |||
| 21 | + | go func() { | |||
| 22 | + | for { | |||
| 23 | + | currentTime := time.Now().Format(time.RFC3339) | |||
| 24 | + | if err := SessionCleaner(a, currentTime); err != nil { | |||
| 25 | + | log.Printf("Failed to clean up sessions: %v", err) | |||
| 26 | + | } | |||
| 27 | + | time.Sleep(1 * time.Hour) | |||
| 28 | + | } | |||
| 29 | + | }() | |||
| 30 | + | } | |||
M internal/server/sqliteclose.go +1 -2
| @@ -10,8 +10,7 @@ func CloseDB(a *App) error { | |||
| 10 | 10 | } | |
| 11 | 11 | ||
| 12 | 12 | func saveDB(a *App) error { | |
| 13 | - | // TODO: Backup the database file if there is any modification. | |
| 14 | - | backupDBFile(a) | |
| 13 | + | ||
| 15 | 14 | return nil | |
| 16 | 15 | } | |
| 17 | 16 | ||
| @@ -10,8 +10,7 @@ func CloseDB(a *App) error { | |||||
| 10 | } | 10 | } | ||
| 11 | 11 | ||||
| 12 | func saveDB(a *App) error { | 12 | func saveDB(a *App) error { | ||
| 13 | - | // TODO: Backup the database file if there is any modification. | 13 | + | |
| 14 | - | backupDBFile(a) | |||
| 15 | return nil | 14 | return nil | ||
| 16 | } | 15 | } | ||
| 17 | 16 | ||||
M internal/server/sqliteinit.go +13 -0
| @@ -100,6 +100,10 @@ func bootstrapDB(dbPath string, app *App) error { | |||
| 100 | 100 | return err | |
| 101 | 101 | } | |
| 102 | 102 | ||
| 103 | + | if err := pkgsqlite.CreateTable(db, tables.Clients, "clients"); err != nil { | |
| 104 | + | return err | |
| 105 | + | } | |
| 106 | + | ||
| 103 | 107 | return nil | |
| 104 | 108 | } | |
| 105 | 109 | ||
| @@ -123,6 +127,10 @@ type SessionsWrapper struct { | |||
| 123 | 127 | *db.Sessions | |
| 124 | 128 | } | |
| 125 | 129 | ||
| 130 | + | type ClientsWrapper struct { | |
| 131 | + | *db.Clients | |
| 132 | + | } | |
| 133 | + | ||
| 126 | 134 | func populateTableFromDB(a *App, query string, table DBTable) error { | |
| 127 | 135 | rows, err := a.DB.Query(query) | |
| 128 | 136 | if err != nil { | |
| @@ -155,6 +163,10 @@ func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error { | |||
| 155 | 163 | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline) | |
| 156 | 164 | } | |
| 157 | 165 | ||
| 166 | + | func (c *ClientsWrapper) ScanFromRows(rows *sql.Rows) error { | |
| 167 | + | return rows.Scan(&c.ID, &c.AccountID, &c.OS, &c.IP, &c.Hostname, &c.Expires) | |
| 168 | + | } | |
| 169 | + | ||
| 158 | 170 | // PopulateStructWithTables updates db struct with tables | |
| 159 | 171 | func PopulateStructWithTables(a *App) error { | |
| 160 | 172 | tables := map[string]DBTable{ | |
| @@ -162,6 +174,7 @@ func PopulateStructWithTables(a *App) error { | |||
| 162 | 174 | "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account}, | |
| 163 | 175 | "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider}, | |
| 164 | 176 | "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions": &SessionsWrapper{&a.DBTables.Sessions}, | |
| 177 | + | "SELECT id, account_id, os, ip, hostname, expires FROM clients": &ClientsWrapper{&a.DBTables.Clients}, | |
| 165 | 178 | } | |
| 166 | 179 | ||
| 167 | 180 | for query, table := range tables { | |
| @@ -100,6 +100,10 @@ func bootstrapDB(dbPath string, app *App) error { | |||||
| 100 | return err | 100 | return err | ||
| 101 | } | 101 | } | ||
| 102 | 102 | ||||
| 103 | + | if err := pkgsqlite.CreateTable(db, tables.Clients, "clients"); err != nil { | |||
| 104 | + | return err | |||
| 105 | + | } | |||
| 106 | + | ||||
| 103 | return nil | 107 | return nil | ||
| 104 | } | 108 | } | ||
| 105 | 109 | ||||
| @@ -123,6 +127,10 @@ type SessionsWrapper struct { | |||||
| 123 | *db.Sessions | 127 | *db.Sessions | ||
| 124 | } | 128 | } | ||
| 125 | 129 | ||||
| 130 | + | type ClientsWrapper struct { | |||
| 131 | + | *db.Clients | |||
| 132 | + | } | |||
| 133 | + | ||||
| 126 | func populateTableFromDB(a *App, query string, table DBTable) error { | 134 | func populateTableFromDB(a *App, query string, table DBTable) error { | ||
| 127 | rows, err := a.DB.Query(query) | 135 | rows, err := a.DB.Query(query) | ||
| 128 | if err != nil { | 136 | if err != nil { | ||
| @@ -155,6 +163,10 @@ func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error { | |||||
| 155 | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline) | 163 | return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline) | ||
| 156 | } | 164 | } | ||
| 157 | 165 | ||||
| 166 | + | func (c *ClientsWrapper) ScanFromRows(rows *sql.Rows) error { | |||
| 167 | + | return rows.Scan(&c.ID, &c.AccountID, &c.OS, &c.IP, &c.Hostname, &c.Expires) | |||
| 168 | + | } | |||
| 169 | + | ||||
| 158 | // PopulateStructWithTables updates db struct with tables | 170 | // PopulateStructWithTables updates db struct with tables | ||
| 159 | func PopulateStructWithTables(a *App) error { | 171 | func PopulateStructWithTables(a *App) error { | ||
| 160 | tables := map[string]DBTable{ | 172 | tables := map[string]DBTable{ | ||
| @@ -162,6 +174,7 @@ func PopulateStructWithTables(a *App) error { | |||||
| 162 | "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account}, | 174 | "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account}, | ||
| 163 | "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider}, | 175 | "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider}, | ||
| 164 | "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions": &SessionsWrapper{&a.DBTables.Sessions}, | 176 | "SELECT id, account_id, access_token, browser_info, expires, is_online FROM sessions": &SessionsWrapper{&a.DBTables.Sessions}, | ||
| 177 | + | "SELECT id, account_id, os, ip, hostname, expires FROM clients": &ClientsWrapper{&a.DBTables.Clients}, | |||
| 165 | } | 178 | } | ||
| 166 | 179 | ||||
| 167 | for query, table := range tables { | 180 | for query, table := range tables { | ||