detached 2b661a6

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 {
4242 return
4343 }
4444
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
4646 if !strings.Contains(imageName, ":") {
4747 imageName += ":latest"
4848 }
@@ -54,19 +54,21 @@ func NewPushCommand(a *cli.App) *cobra.Command {
5454 return
5555 }
5656
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)
5858 match, _ = regexp.MatchString("^(https?:\\/\\/)?([a-zA-Z0-9\\-_\\.]+\\.)+[a-zA-Z0-9\\-_\\.]+(:[0-9]+)?$", targetDomain)
5959 if !match {
6060 fmt.Println("You must specify a valid target domain in the form http(s)://domain.tld, if no protocol is specified, HTTPS is used")
6161 return
6262 }
6363
64+ // Get the image ID
6465 imageID, err := docker.GetImageID(imageName)
6566 if err != nil {
6667 fmt.Println("Error getting image ID:", err)
6768 return
6869 }
6970
71+ // Get the image size
7072 totalSize, err := docker.GetImageSize(imageID)
7173 if err != nil {
7274 fmt.Println("Error estimating image size:", err)
@@ -42,7 +42,7 @@ func NewPushCommand(a *cli.App) *cobra.Command {
42 return42 return
43 }43 }
44 44
45- // If there is :tag at the end of the image name, we append :latest45+ // 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 return54 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 return61 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 return68 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 {
2020 log.Fatal("Failed to initialize database:", err)
2121 }
2222
23+ // Start the session cleaner cron job
24+ a.StartSessionCleaner()
25+
2326 _, err = server.HandleNewTokenInitialization(a)
2427 if err != nil {
2528 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 {
1212 UserID string `sql:"user_id, foreign_key=user.id"`
1313 Sessions []Sessions
1414 Providers []Provider
15+ Clients []Clients
1516 }
1617
1718 type Sessions struct {
@@ -32,3 +33,12 @@ type Provider struct {
3233 ProfileURL string `sql:"profile_url"`
3334 Email string `sql:"email"`
3435 }
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 []Sessions13 Sessions []Sessions
14 Providers []Provider14 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
8282 return nil
8383 }
8484
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-
9685 // DeleteSession deletes the session from the database.
9786 func DeleteDBSession(a *server.App, accountID string, sessionID string) error {
9887 query := "DELETE FROM sessions WHERE account_id = ? AND id = ?"
@@ -121,3 +110,13 @@ func GetSessionExpiration(a *server.App, accountID string, sessionID string, cur
121110
122111 return expirationTimeParsed, nil
123112 }
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 nil82 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, nil111 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 @@
11 package handler
22
33 import (
4+ "io/fs"
45 "net/http"
56 "os"
67
@@ -8,17 +9,47 @@ import (
89 "github.com/labstack/echo/v4"
910 )
1011
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+
1136 // StaticRoute serves static files from the embedded filesystem
1237 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" {
1540 c.Response().Header().Set("Cache-Control", "public, max-age=86400")
1641 } else {
1742 c.Response().Header().Set("Cache-Control", "no-cache")
1843 }
1944
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)
2253 publicFS.ServeHTTP(c.Response(), c.Request())
2354 return nil
2455 }
@@ -1,6 +1,7 @@
1 package handler1 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 filesystem36 // 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 variable38+ // 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 filesystem45+ // 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 nil54 return nil
24 }55 }
M internal/httpserve/middleware/requirelogin.go +16 -63
@@ -18,7 +18,7 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
1818 return func(c echo.Context) error {
1919 if err := validateSessionAndUser(c, a); err != nil {
2020 log.Println("Session validation failed:", err)
21- return redirectToLogin(c, a)
21+ return DeleteCookieAndRedirectToLogin(c, a)
2222 }
2323 return next(c)
2424 }
@@ -26,52 +26,37 @@ func RequireLogin(a *server.App) echo.MiddlewareFunc {
2626 }
2727
2828 func validateSessionAndUser(c echo.Context, a *server.App) error {
29+ sessionExpiryTime := 30 * time.Minute
30+
2931 sess, err := session.Get("session", c)
3032 if err != nil {
3133 return err
3234 }
3335
34- // Retrieve accountID and sessionID from session values
3536 accountID, ok := sess.Values["accountID"].(string)
3637 if !ok {
37- return fmt.Errorf("invalid account ID type")
38+ return fmt.Errorf("accountID not found or invalid type")
3839 }
3940
4041 sessionID, ok := sess.Values["sessionID"].(string)
4142 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")
5344 }
5445
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")
5849 }
5950
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")
6354 }
6455
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)
7560 }
7661
7762 return nil
@@ -81,17 +66,6 @@ func redirectToLogin(c echo.Context, a *server.App) error {
8166 return c.Redirect(http.StatusSeeOther, a.Config.Admin.Path+"/login")
8267 }
8368
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-
9569 func isSessionExpiredInDB(a *server.App, userID string, sessionID string) (bool, error) {
9670 currentTime := time.Now()
9771 expirationTime, err := queries.GetSessionExpiration(a, userID, sessionID, currentTime)
@@ -111,28 +85,7 @@ func isAccountIDInDB(a *server.App, accountID string) (bool, error) {
11185 return accountExists, nil
11286 }
11387
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 {
13689 // Delete session cookie
13790 cookie := new(http.Cookie)
13891 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 err33 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 err48+ 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 err53+ 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 err58+ 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 nil62 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, nil85 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 cookie89 // 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 (
1010
1111 // RegisterRoutes registers all routes for the application
1212 func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
13- // --- Register API endpoints --- //
14- bindAPIEndpoints(e, a)
1513 AdminPath := a.Config.Admin.Path
14+ bindLoginRoute(e, a, AdminPath)
15+ bindAPIEndpoints(e, a)
16+
1617 // SetCommonDataMiddleware will pass data to the renderer
1718 e.Use(middleware.SetCommonDataMiddleware(a))
1819 // Error handler middleware
1920 e.Use(middleware.ErrorHandler)
20-
2121 // Initiate the session middleware
2222 e.Use(middleware.InitSessionMiddleware(a))
2323 // Use middlewares
2424 e.Use(middleware.SecureRoutes())
25-
2625 // Color scheme detection for dark/light mode
2726 e.Use(middleware.ColorSchemeDetection)
2827 // Language detection
@@ -31,7 +30,6 @@ func RegisterRoutes(e *echo.Echo, a *server.App) *echo.Echo {
3130 // --- Register routes --- //
3231 bindAdminRoute(e, a, AdminPath)
3332 bindStaticRoute(e, a, "/*")
34- bindLoginRoute(e, a, AdminPath)
3533 bindHTMXEndpoints(e, a)
3634 // Protect the root path with a 403
3735 e.GET("/", func(c echo.Context) error {
@@ -10,19 +10,18 @@ import (
10 10
11 // RegisterRoutes registers all routes for the application11 // 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.Path13 AdminPath := a.Config.Admin.Path
14+ bindLoginRoute(e, a, AdminPath)
15+ bindAPIEndpoints(e, a)
16+
16 // SetCommonDataMiddleware will pass data to the renderer17 // SetCommonDataMiddleware will pass data to the renderer
17 e.Use(middleware.SetCommonDataMiddleware(a))18 e.Use(middleware.SetCommonDataMiddleware(a))
18 // Error handler middleware19 // Error handler middleware
19 e.Use(middleware.ErrorHandler)20 e.Use(middleware.ErrorHandler)
20-
21 // Initiate the session middleware21 // Initiate the session middleware
22 e.Use(middleware.InitSessionMiddleware(a))22 e.Use(middleware.InitSessionMiddleware(a))
23 // Use middlewares23 // Use middlewares
24 e.Use(middleware.SecureRoutes())24 e.Use(middleware.SecureRoutes())
25-
26 // Color scheme detection for dark/light mode25 // Color scheme detection for dark/light mode
27 e.Use(middleware.ColorSchemeDetection)26 e.Use(middleware.ColorSchemeDetection)
28 // Language detection27 // 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 40334 // 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 {
3030 Account db.Account `sql:"account"`
3131 Sessions db.Sessions `sql:"sessions"`
3232 Provider db.Provider `sql:"provider"`
33+ Clients db.Clients `sql:"clients"`
3334 }
3435
3536 // 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 URL36 // 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 {
1010 }
1111
1212 func saveDB(a *App) error {
13- // TODO: Backup the database file if there is any modification.
14- backupDBFile(a)
13+
1514 return nil
1615 }
1716
@@ -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 nil14 return nil
16 }15 }
17 16
M internal/server/sqliteinit.go +13 -0
@@ -100,6 +100,10 @@ func bootstrapDB(dbPath string, app *App) error {
100100 return err
101101 }
102102
103+ if err := pkgsqlite.CreateTable(db, tables.Clients, "clients"); err != nil {
104+ return err
105+ }
106+
103107 return nil
104108 }
105109
@@ -123,6 +127,10 @@ type SessionsWrapper struct {
123127 *db.Sessions
124128 }
125129
130+type ClientsWrapper struct {
131+ *db.Clients
132+}
133+
126134 func populateTableFromDB(a *App, query string, table DBTable) error {
127135 rows, err := a.DB.Query(query)
128136 if err != nil {
@@ -155,6 +163,10 @@ func (s *SessionsWrapper) ScanFromRows(rows *sql.Rows) error {
155163 return rows.Scan(&s.ID, &s.AccountID, &s.AccessToken, &s.BrowserInfo, &s.Expires, &s.IsOnline)
156164 }
157165
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+
158170 // PopulateStructWithTables updates db struct with tables
159171 func PopulateStructWithTables(a *App) error {
160172 tables := map[string]DBTable{
@@ -162,6 +174,7 @@ func PopulateStructWithTables(a *App) error {
162174 "SELECT id, user_id FROM account": &AccountWrapper{&a.DBTables.Account},
163175 "SELECT id, account_id, name, login, avatar_url, profile_url, email FROM provider": &ProviderWrapper{&a.DBTables.Provider},
164176 "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},
165178 }
166179
167180 for query, table := range tables {
@@ -100,6 +100,10 @@ func bootstrapDB(dbPath string, app *App) error {
100 return err100 return err
101 }101 }
102 102
103+ if err := pkgsqlite.CreateTable(db, tables.Clients, "clients"); err != nil {
104+ return err
105+ }
106+
103 return nil107 return nil
104 }108 }
105 109
@@ -123,6 +127,10 @@ type SessionsWrapper struct {
123 *db.Sessions127 *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 tables170 // 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 {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close