From f1e2b55cd1d0341ffb7c46ed81f919f6eb82077a Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 31 Dec 2025 21:04:08 +0200 Subject: [PATCH 01/12] fix: add rate limiting in the forward auth endpoint (#555) --- internal/controller/user_controller.go | 57 +++++++++-------------- internal/middleware/context_middleware.go | 15 ++++++ 2 files changed, 38 insertions(+), 34 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index a607c4a..5670dd2 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -3,6 +3,7 @@ package controller import ( "fmt" "strings" + "time" "github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/service" @@ -60,23 +61,17 @@ func (controller *UserController) loginHandler(c *gin.Context) { return } - clientIP := c.ClientIP() + log.Debug().Str("username", req.Username).Msg("Login attempt") - rateIdentifier := req.Username - - if rateIdentifier == "" { - rateIdentifier = clientIP - } - - log.Debug().Str("username", req.Username).Str("ip", clientIP).Msg("Login attempt") - - isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) + isLocked, remaining := controller.auth.IsAccountLocked(req.Username) if isLocked { - log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed login attempts") + log.Warn().Str("username", req.Username).Msg("Account is locked due to too many failed login attempts") + c.Writer.Header().Add("x-tinyauth-lock-locked", "true") + c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) c.JSON(429, gin.H{ "status": 429, - "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remainingTime), + "message": fmt.Sprintf("Too many failed login attempts. Try again in %d seconds", remaining), }) return } @@ -84,8 +79,8 @@ func (controller *UserController) loginHandler(c *gin.Context) { userSearch := controller.auth.SearchUser(req.Username) if userSearch.Type == "unknown" { - log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("User not found") - controller.auth.RecordLoginAttempt(rateIdentifier, false) + log.Warn().Str("username", req.Username).Msg("User not found") + controller.auth.RecordLoginAttempt(req.Username, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -94,8 +89,8 @@ func (controller *UserController) loginHandler(c *gin.Context) { } if !controller.auth.VerifyUser(userSearch, req.Password) { - log.Warn().Str("username", req.Username).Str("ip", clientIP).Msg("Invalid password") - controller.auth.RecordLoginAttempt(rateIdentifier, false) + log.Warn().Str("username", req.Username).Msg("Invalid password") + controller.auth.RecordLoginAttempt(req.Username, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -103,9 +98,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { return } - log.Info().Str("username", req.Username).Str("ip", clientIP).Msg("Login successful") + log.Info().Str("username", req.Username).Msg("Login successful") - controller.auth.RecordLoginAttempt(rateIdentifier, true) + controller.auth.RecordLoginAttempt(req.Username, true) if userSearch.Type == "local" { user := controller.auth.GetLocalUser(userSearch.Username) @@ -209,23 +204,17 @@ func (controller *UserController) totpHandler(c *gin.Context) { return } - clientIP := c.ClientIP() + log.Debug().Str("username", context.Username).Msg("TOTP verification attempt") - rateIdentifier := context.Username - - if rateIdentifier == "" { - rateIdentifier = clientIP - } - - log.Debug().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification attempt") - - isLocked, remainingTime := controller.auth.IsAccountLocked(rateIdentifier) + isLocked, remaining := controller.auth.IsAccountLocked(context.Username) if isLocked { - log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Account is locked due to too many failed TOTP attempts") + log.Warn().Str("username", context.Username).Msg("Account is locked due to too many failed TOTP attempts") + c.Writer.Header().Add("x-tinyauth-lock-locked", "true") + c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) c.JSON(429, gin.H{ "status": 429, - "message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remainingTime), + "message": fmt.Sprintf("Too many failed TOTP attempts. Try again in %d seconds", remaining), }) return } @@ -235,8 +224,8 @@ func (controller *UserController) totpHandler(c *gin.Context) { ok := totp.Validate(req.Code, user.TotpSecret) if !ok { - log.Warn().Str("username", context.Username).Str("ip", clientIP).Msg("Invalid TOTP code") - controller.auth.RecordLoginAttempt(rateIdentifier, false) + log.Warn().Str("username", context.Username).Msg("Invalid TOTP code") + controller.auth.RecordLoginAttempt(context.Username, false) c.JSON(401, gin.H{ "status": 401, "message": "Unauthorized", @@ -244,9 +233,9 @@ func (controller *UserController) totpHandler(c *gin.Context) { return } - log.Info().Str("username", context.Username).Str("ip", clientIP).Msg("TOTP verification successful") + log.Info().Str("username", context.Username).Msg("TOTP verification successful") - controller.auth.RecordLoginAttempt(rateIdentifier, true) + controller.auth.RecordLoginAttempt(context.Username, true) sessionCookie := config.SessionCookie{ Username: user.Username, diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 16f7c05..a6bddc9 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -3,6 +3,7 @@ package middleware import ( "fmt" "strings" + "time" "github.com/steveiliop56/tinyauth/internal/config" "github.com/steveiliop56/tinyauth/internal/service" @@ -116,20 +117,34 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } + locked, remaining := m.auth.IsAccountLocked(basic.Username) + + if locked { + log.Debug().Msgf("Account for user %s is locked for %d seconds, denying auth", basic.Username, remaining) + c.Writer.Header().Add("x-tinyauth-lock-locked", "true") + c.Writer.Header().Add("x-tinyauth-lock-reset", time.Now().Add(time.Duration(remaining)*time.Second).Format(time.RFC3339)) + c.Next() + return + } + userSearch := m.auth.SearchUser(basic.Username) if userSearch.Type == "unknown" || userSearch.Type == "error" { + m.auth.RecordLoginAttempt(basic.Username, false) log.Debug().Msg("User from basic auth not found") c.Next() return } if !m.auth.VerifyUser(userSearch, basic.Password) { + m.auth.RecordLoginAttempt(basic.Username, false) log.Debug().Msg("Invalid password for basic auth user") c.Next() return } + m.auth.RecordLoginAttempt(basic.Username, true) + switch userSearch.Type { case "local": log.Debug().Msg("Basic auth user is local") From 721f302c0bede673c27e52fef2c9836f458e38dd Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 7 Jan 2026 13:25:39 +0200 Subject: [PATCH 02/12] chore: fix typo in example env --- .env.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 2c6c4fe..6bf3d45 100644 --- a/.env.example +++ b/.env.example @@ -33,15 +33,15 @@ TINYAUTH_SERVER_TRUSTEDPROXIES="" # Format: username:bcrypt_hash (use bcrypt to generate hash) TINYAUTH_AUTH_USERS="admin:$2a$10$example_bcrypt_hash_here" # Path to external users file (optional) -TINYAUTH_USERSFILE="" +TINYAUTH_AUTH_USERSFILE="" # Enable secure cookies (requires HTTPS) -TINYAUTH_SECURECOOKIE="true" +TINYAUTH_AUTH_SECURECOOKIE="true" # Session expiry in seconds (7200 = 2 hours) -TINYAUTH_SESSIONEXPIRY="7200" +TINYAUTH_AUTH_SESSIONEXPIRY="7200" # Login timeout in seconds (300 = 5 minutes) -TINYAUTH_LOGINTIMEOUT="300" +TINYAUTH_AUTH_LOGINTIMEOUT="300" # Maximum login retries before lockout -TINYAUTH_LOGINMAXRETRIES="5" +TINYAUTH_AUTH_LOGINMAXRETRIES="5" # OAuth Configuration From e7bd64d7a3dc9768657c67e7bfa816a17a4bd92d Mon Sep 17 00:00:00 2001 From: Pushpinder Singh <53684951+pushpinderbal@users.noreply.github.com> Date: Wed, 7 Jan 2026 06:37:23 -0500 Subject: [PATCH 03/12] feat: add session max lifetime and fix refresh logic (#559) * feat: allow any HTTP method for /api/auth/envoy and restrict methods for non-envoy proxies * feat: add Allow header for invalid methods in proxyHandler * feat: add session max lifetime and fix refresh logic * fix: set default value for created_at column and improve session expiration logic --------- Co-authored-by: Stavros --- .env.example | 2 + cmd/tinyauth/tinyauth.go | 7 +-- config.example.yaml | 2 + .../migrations/000004_created_at.down.sql | 1 + .../migrations/000004_created_at.up.sql | 1 + internal/bootstrap/app_bootstrap.go | 4 ++ internal/bootstrap/db_bootstrap.go | 4 ++ internal/bootstrap/service_bootstrap.go | 17 ++++---- internal/config/config.go | 13 +++--- internal/controller/proxy_controller_test.go | 15 ++++--- internal/controller/user_controller_test.go | 15 ++++--- internal/repository/models.go | 1 + internal/repository/query.sql.go | 14 ++++-- internal/service/auth_service.go | 43 ++++++++++++++----- query.sql | 3 +- schema.sql | 1 + 16 files changed, 96 insertions(+), 47 deletions(-) create mode 100644 internal/assets/migrations/000004_created_at.down.sql create mode 100644 internal/assets/migrations/000004_created_at.up.sql diff --git a/.env.example b/.env.example index 6bf3d45..607ce08 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,8 @@ TINYAUTH_AUTH_USERSFILE="" TINYAUTH_AUTH_SECURECOOKIE="true" # Session expiry in seconds (7200 = 2 hours) TINYAUTH_AUTH_SESSIONEXPIRY="7200" +# Session maximum lifetime in seconds (0 = unlimited) +TINYAUTH_AUTH_SESSIONMAXLIFETIME="0" # Login timeout in seconds (300 = 5 minutes) TINYAUTH_AUTH_LOGINTIMEOUT="300" # Maximum login retries before lockout diff --git a/cmd/tinyauth/tinyauth.go b/cmd/tinyauth/tinyauth.go index 33b4015..7d0dbf7 100644 --- a/cmd/tinyauth/tinyauth.go +++ b/cmd/tinyauth/tinyauth.go @@ -25,9 +25,10 @@ func NewTinyauthCmdConfiguration() *config.Config { Address: "0.0.0.0", }, Auth: config.AuthConfig{ - SessionExpiry: 3600, - LoginTimeout: 300, - LoginMaxRetries: 3, + SessionExpiry: 3600, + SessionMaxLifetime: 0, + LoginTimeout: 300, + LoginMaxRetries: 3, }, UI: config.UIConfig{ Title: "Tinyauth", diff --git a/config.example.yaml b/config.example.yaml index 544bc83..26e56d5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -38,6 +38,8 @@ auth: secureCookie: false # Session expiry in seconds (3600 = 1 hour) sessionExpiry: 3600 + # Session maximum lifetime in seconds (0 = unlimited) + sessionMaxLifetime: 0 # Login timeout in seconds (300 = 5 minutes) loginTimeout: 300 # Maximum login retries before lockout diff --git a/internal/assets/migrations/000004_created_at.down.sql b/internal/assets/migrations/000004_created_at.down.sql new file mode 100644 index 0000000..fa7d58a --- /dev/null +++ b/internal/assets/migrations/000004_created_at.down.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" DROP COLUMN "created_at"; diff --git a/internal/assets/migrations/000004_created_at.up.sql b/internal/assets/migrations/000004_created_at.up.sql new file mode 100644 index 0000000..a21e944 --- /dev/null +++ b/internal/assets/migrations/000004_created_at.up.sql @@ -0,0 +1 @@ +ALTER TABLE "sessions" ADD COLUMN "created_at" INTEGER NOT NULL DEFAULT 0; diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 5c45f9c..414beea 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -42,6 +42,10 @@ func NewBootstrapApp(config config.Config) *BootstrapApp { } func (app *BootstrapApp) Setup() error { + // validate session config + if app.config.Auth.SessionMaxLifetime != 0 && app.config.Auth.SessionMaxLifetime < app.config.Auth.SessionExpiry { + return fmt.Errorf("session max lifetime cannot be less than session expiry") + } // Parse users users, err := utils.GetUsers(app.config.Auth.Users, app.config.Auth.UsersFile) diff --git a/internal/bootstrap/db_bootstrap.go b/internal/bootstrap/db_bootstrap.go index ad8f4f6..ab10daa 100644 --- a/internal/bootstrap/db_bootstrap.go +++ b/internal/bootstrap/db_bootstrap.go @@ -27,6 +27,10 @@ func (app *BootstrapApp) SetupDatabase(databasePath string) (*sql.DB, error) { return nil, fmt.Errorf("failed to open database: %w", err) } + // Limit to 1 connection to sequence writes, this may need to be revisited in the future + // if the sqlite connection starts being a bottleneck + db.SetMaxOpenConns(1) + migrations, err := iofs.New(assets.Migrations, "migrations") if err != nil { diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index b41fc62..6f6a088 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -58,14 +58,15 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er services.accessControlService = accessControlsService authService := service.NewAuthService(service.AuthServiceConfig{ - Users: app.context.users, - OauthWhitelist: app.config.OAuth.Whitelist, - SessionExpiry: app.config.Auth.SessionExpiry, - SecureCookie: app.config.Auth.SecureCookie, - CookieDomain: app.context.cookieDomain, - LoginTimeout: app.config.Auth.LoginTimeout, - LoginMaxRetries: app.config.Auth.LoginMaxRetries, - SessionCookieName: app.context.sessionCookieName, + Users: app.context.users, + OauthWhitelist: app.config.OAuth.Whitelist, + SessionExpiry: app.config.Auth.SessionExpiry, + SessionMaxLifetime: app.config.Auth.SessionMaxLifetime, + SecureCookie: app.config.Auth.SecureCookie, + CookieDomain: app.context.cookieDomain, + LoginTimeout: app.config.Auth.LoginTimeout, + LoginMaxRetries: app.config.Auth.LoginMaxRetries, + SessionCookieName: app.context.sessionCookieName, }, dockerService, ldapService, queries) err = authService.Init() diff --git a/internal/config/config.go b/internal/config/config.go index b7fe6e3..2e7af66 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,12 +40,13 @@ type ServerConfig struct { } type AuthConfig struct { - Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` - UsersFile string `description:"Path to the users file." yaml:"usersFile"` - SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` - SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` - LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` - LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` + Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` + UsersFile string `description:"Path to the users file." yaml:"usersFile"` + SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` + SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` + SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"` + LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` + LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` } type OAuthConfig struct { diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index 4b6a7e4..5f44382 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -57,13 +57,14 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test }, }, - OauthWhitelist: "", - SessionExpiry: 3600, - SecureCookie: false, - CookieDomain: "localhost", - LoginTimeout: 300, - LoginMaxRetries: 3, - SessionCookieName: "tinyauth-session", + OauthWhitelist: "", + SessionExpiry: 3600, + SessionMaxLifetime: 0, + SecureCookie: false, + CookieDomain: "localhost", + LoginTimeout: 300, + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", }, dockerService, nil, queries) // Controller diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index ff95a3c..dc8ec5c 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -60,13 +60,14 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng TotpSecret: totpSecret, }, }, - OauthWhitelist: "", - SessionExpiry: 3600, - SecureCookie: false, - CookieDomain: "localhost", - LoginTimeout: 300, - LoginMaxRetries: 3, - SessionCookieName: "tinyauth-session", + OauthWhitelist: "", + SessionExpiry: 3600, + SessionMaxLifetime: 0, + SecureCookie: false, + CookieDomain: "localhost", + LoginTimeout: 300, + LoginMaxRetries: 3, + SessionCookieName: "tinyauth-session", }, nil, nil, queries) // Controller diff --git a/internal/repository/models.go b/internal/repository/models.go index 0f5195e..61f7f80 100644 --- a/internal/repository/models.go +++ b/internal/repository/models.go @@ -13,6 +13,7 @@ type Session struct { TotpPending bool OAuthGroups string Expiry int64 + CreatedAt int64 OAuthName string OAuthSub string } diff --git a/internal/repository/query.sql.go b/internal/repository/query.sql.go index 110bd1b..5924842 100644 --- a/internal/repository/query.sql.go +++ b/internal/repository/query.sql.go @@ -19,12 +19,13 @@ INSERT INTO sessions ( "totp_pending", "oauth_groups", "expiry", + "created_at", "oauth_name", "oauth_sub" ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) -RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub +RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub ` type CreateSessionParams struct { @@ -36,6 +37,7 @@ type CreateSessionParams struct { TotpPending bool OAuthGroups string Expiry int64 + CreatedAt int64 OAuthName string OAuthSub string } @@ -50,6 +52,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S arg.TotpPending, arg.OAuthGroups, arg.Expiry, + arg.CreatedAt, arg.OAuthName, arg.OAuthSub, ) @@ -63,6 +66,7 @@ func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (S &i.TotpPending, &i.OAuthGroups, &i.Expiry, + &i.CreatedAt, &i.OAuthName, &i.OAuthSub, ) @@ -90,7 +94,7 @@ func (q *Queries) DeleteSession(ctx context.Context, uuid string) error { } const getSession = `-- name: GetSession :one -SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub FROM "sessions" +SELECT uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub FROM "sessions" WHERE "uuid" = ? ` @@ -106,6 +110,7 @@ func (q *Queries) GetSession(ctx context.Context, uuid string) (Session, error) &i.TotpPending, &i.OAuthGroups, &i.Expiry, + &i.CreatedAt, &i.OAuthName, &i.OAuthSub, ) @@ -124,7 +129,7 @@ UPDATE "sessions" SET "oauth_name" = ?, "oauth_sub" = ? WHERE "uuid" = ? -RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, oauth_name, oauth_sub +RETURNING uuid, username, email, name, provider, totp_pending, oauth_groups, expiry, created_at, oauth_name, oauth_sub ` type UpdateSessionParams struct { @@ -163,6 +168,7 @@ func (q *Queries) UpdateSession(ctx context.Context, arg UpdateSessionParams) (S &i.TotpPending, &i.OAuthGroups, &i.Expiry, + &i.CreatedAt, &i.OAuthName, &i.OAuthSub, ) diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index e823e2a..e71fd5f 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -26,14 +26,15 @@ type LoginAttempt struct { } type AuthServiceConfig struct { - Users []config.User - OauthWhitelist string - SessionExpiry int - SecureCookie bool - CookieDomain string - LoginTimeout int - LoginMaxRetries int - SessionCookieName string + Users []config.User + OauthWhitelist string + SessionExpiry int + SessionMaxLifetime int + SecureCookie bool + CookieDomain string + LoginTimeout int + LoginMaxRetries int + SessionCookieName string } type AuthService struct { @@ -212,6 +213,7 @@ func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.Sessio TotpPending: data.TotpPending, OAuthGroups: data.OAuthGroups, Expiry: time.Now().Add(time.Duration(expiry) * time.Second).Unix(), + CreatedAt: time.Now().Unix(), OAuthName: data.OAuthName, OAuthSub: data.OAuthSub, } @@ -242,11 +244,19 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error { currentTime := time.Now().Unix() - if session.Expiry-currentTime > int64(time.Hour.Seconds()) { + var refreshThreshold int64 + + if auth.config.SessionExpiry <= int(time.Hour.Seconds()) { + refreshThreshold = int64(auth.config.SessionExpiry / 2) + } else { + refreshThreshold = int64(time.Hour.Seconds()) + } + + if session.Expiry-currentTime > refreshThreshold { return nil } - newExpiry := currentTime + int64(time.Hour.Seconds()) + newExpiry := session.Expiry + refreshThreshold _, err = auth.queries.UpdateSession(c, repository.UpdateSessionParams{ Username: session.Username, @@ -265,7 +275,8 @@ func (auth *AuthService) RefreshSessionCookie(c *gin.Context) error { return err } - c.SetCookie(auth.config.SessionCookieName, cookie, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) + c.SetCookie(auth.config.SessionCookieName, cookie, int(newExpiry-currentTime), "/", fmt.Sprintf(".%s", auth.config.CookieDomain), auth.config.SecureCookie, true) + log.Trace().Str("username", session.Username).Msg("Session cookie refreshed") return nil } @@ -306,6 +317,16 @@ func (auth *AuthService) GetSessionCookie(c *gin.Context) (config.SessionCookie, currentTime := time.Now().Unix() + if auth.config.SessionMaxLifetime != 0 && session.CreatedAt != 0 { + if currentTime-session.CreatedAt > int64(auth.config.SessionMaxLifetime) { + err = auth.queries.DeleteSession(c, cookie) + if err != nil { + log.Error().Err(err).Msg("Failed to delete session exceeding max lifetime") + } + return config.SessionCookie{}, fmt.Errorf("session expired due to max lifetime exceeded") + } + } + if currentTime > session.Expiry { err = auth.queries.DeleteSession(c, cookie) if err != nil { diff --git a/query.sql b/query.sql index 36d0e7f..9fde4e2 100644 --- a/query.sql +++ b/query.sql @@ -8,10 +8,11 @@ INSERT INTO sessions ( "totp_pending", "oauth_groups", "expiry", + "created_at", "oauth_name", "oauth_sub" ) VALUES ( - ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) RETURNING *; diff --git a/schema.sql b/schema.sql index 4221930..a7f37eb 100644 --- a/schema.sql +++ b/schema.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS "sessions" ( "totp_pending" BOOLEAN NOT NULL, "oauth_groups" TEXT NULL, "expiry" INTEGER NOT NULL, + "created_at" INTEGER NOT NULL, "oauth_name" TEXT NULL, "oauth_sub" TEXT NULL ); From 9f52d13028b58d3481e2355d6947dc6919cea530 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:37:45 +0200 Subject: [PATCH 04/12] chore(deps): bump the minor-patch group across 1 directory with 2 updates (#560) Bumps the minor-patch group with 2 updates in the / directory: [github.com/weppos/publicsuffix-go](https://github.com/weppos/publicsuffix-go) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite). Updates `github.com/weppos/publicsuffix-go` from 0.50.1 to 0.50.2 - [Changelog](https://github.com/weppos/publicsuffix-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/weppos/publicsuffix-go/compare/v0.50.1...v0.50.2) Updates `modernc.org/sqlite` from 1.38.2 to 1.42.2 - [Commits](https://gitlab.com/cznic/sqlite/compare/v1.38.2...v1.42.2) --- updated-dependencies: - dependency-name: github.com/weppos/publicsuffix-go dependency-version: 0.50.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: modernc.org/sqlite dependency-version: 1.42.2 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 8 ++++---- go.sum | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 2126dc4..959ed13 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,12 @@ require ( github.com/pquerna/otp v1.5.0 github.com/rs/zerolog v1.34.0 github.com/traefik/paerser v0.2.2 - github.com/weppos/publicsuffix-go v0.50.1 + github.com/weppos/publicsuffix-go v0.50.2 golang.org/x/crypto v0.46.0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b golang.org/x/oauth2 v0.34.0 gotest.tools/v3 v3.5.2 - modernc.org/sqlite v1.38.2 + modernc.org/sqlite v1.42.2 ) require ( @@ -112,14 +112,14 @@ require ( go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect golang.org/x/arch v0.20.0 // indirect - golang.org/x/net v0.47.0 // indirect + golang.org/x/net v0.48.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.66.3 // indirect + modernc.org/libc v1.66.10 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect rsc.io/qr v0.2.0 // indirect diff --git a/go.sum b/go.sum index b04ac5b..dc7387a 100644 --- a/go.sum +++ b/go.sum @@ -267,8 +267,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -github.com/weppos/publicsuffix-go v0.50.1 h1:elrBHeSkS/eIb169+DnLrknqmdP4AjT0Q0tEdytz1Og= -github.com/weppos/publicsuffix-go v0.50.1/go.mod h1:znn0JVXjcR5hpUl9pbEogwH6I710rA1AX0QQPT0bf+k= +github.com/weppos/publicsuffix-go v0.50.2 h1:KsJFc8IEKTJovM46SRCnGNsM+rFShxcs6VEHjOJcXzE= +github.com/weppos/publicsuffix-go v0.50.2/go.mod h1:CbQCKDtXF8UcT7hrxeMa0MDjwhpOI9iYOU7cfq+yo8k= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -310,8 +310,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -369,18 +369,18 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= -modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= -modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= -modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= -modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4= +modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A= +modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= -modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= -modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A= +modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= @@ -389,8 +389,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= -modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= -modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/sqlite v1.42.2 h1:7hkZUNJvJFN2PgfUdjni9Kbvd4ef4mNLOu0B9FGxM74= +modernc.org/sqlite v1.42.2/go.mod h1:+VkC6v3pLOAE0A0uVucQEcbVW0I5nHCeDaBf+DpsQT8= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= From 23987aade8cdcf6e5163827c3d7ae77bc023c359 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:38:08 +0200 Subject: [PATCH 05/12] chore(deps): bump the minor-patch group across 1 directory with 5 updates (#566) Bumps the minor-patch group with 5 updates in the /frontend directory: | Package | From | To | | --- | --- | --- | | [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.69.0` | `7.70.0` | | [react-i18next](https://github.com/i18next/react-i18next) | `16.5.0` | `16.5.1` | | [zod](https://github.com/colinhacks/zod) | `4.3.2` | `4.3.5` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.51.0` | `8.52.0` | | [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `7.3.0` | `7.3.1` | Updates `react-hook-form` from 7.69.0 to 7.70.0 - [Release notes](https://github.com/react-hook-form/react-hook-form/releases) - [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md) - [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.69.0...v7.70.0) Updates `react-i18next` from 16.5.0 to 16.5.1 - [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/react-i18next/compare/v16.5.0...v16.5.1) Updates `zod` from 4.3.2 to 4.3.5 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.3.2...v4.3.5) Updates `typescript-eslint` from 8.51.0 to 8.52.0 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.52.0/packages/typescript-eslint) Updates `vite` from 7.3.0 to 7.3.1 - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v7.3.1/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v7.3.1/packages/vite) --- updated-dependencies: - dependency-name: react-hook-form dependency-version: 7.70.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: react-i18next dependency-version: 16.5.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: zod dependency-version: 4.3.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: typescript-eslint dependency-version: 8.52.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-patch - dependency-name: vite dependency-version: 7.3.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 92 +++++++++++++++++++++++++++---------------- frontend/package.json | 10 ++--- 2 files changed, 62 insertions(+), 40 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index f5a997d..6bd74cc 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -24,14 +24,14 @@ "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-hook-form": "^7.69.0", - "react-i18next": "^16.5.0", + "react-hook-form": "^7.70.0", + "react-i18next": "^16.5.1", "react-markdown": "^10.1.0", "react-router": "^7.11.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", - "zod": "^4.3.2", + "zod": "^4.3.5", }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -47,8 +47,8 @@ "prettier": "3.7.4", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0", + "typescript-eslint": "^8.52.0", + "vite": "^7.3.1", }, }, }, @@ -373,25 +373,25 @@ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.51.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/type-utils": "8.51.0", "@typescript-eslint/utils": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.51.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.51.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.51.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.51.0", "@typescript-eslint/types": "^8.51.0", "debug": "^4.3.4" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1" } }, "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.51.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0", "debug": "^4.3.4", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.46.1", "", {}, "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.51.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.51.0", "@typescript-eslint/tsconfig-utils": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.2.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.46.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -795,9 +795,9 @@ "react-dom": ["react-dom@19.2.3", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg=="], - "react-hook-form": ["react-hook-form@7.69.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-yt6ZGME9f4F6WHwevrvpAjh42HMvocuSnSIHUGycBqXIJdhqGSPQzTpGF+1NLREk/58IdPxEMfPcFCjlMhclGw=="], + "react-hook-form": ["react-hook-form@7.70.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-COOMajS4FI3Wuwrs3GPpi/Jeef/5W1DRR84Yl5/ShlT3dKVFUfoGiEZ/QE6Uw8P4T2/CLJdcTVYKvWBMQTEpvw=="], - "react-i18next": ["react-i18next@16.5.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-IMpPTyCTKxEj8klCrLKUTIUa8uYTd851+jcu2fJuUB9Agkk9Qq8asw4omyeHVnOXHrLgQJGTm5zTvn8HpaPiqw=="], + "react-i18next": ["react-i18next@16.5.1", "", { "dependencies": { "@babel/runtime": "^7.28.4", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-Hks6UIRZWW4c+qDAnx1csVsCGYeIR4MoBGQgJ+NUoNnO6qLxXuf8zu0xdcinyXUORgGzCdRsexxO1Xzv3sTdnw=="], "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="], @@ -863,7 +863,7 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "ts-api-utils": ["ts-api-utils@2.3.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-6eg3Y9SF7SsAvGzRHQvvc1skDAhwI4YQ32ui1scxD1Ccr0G5qIIbUBT3pFTKX8kmWIQClHobtUdNuaBgwdfdWg=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], @@ -873,7 +873,7 @@ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "typescript-eslint": ["typescript-eslint@8.51.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.51.0", "@typescript-eslint/parser": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0", "@typescript-eslint/utils": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA=="], + "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], @@ -903,7 +903,7 @@ "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - "vite": ["vite@7.3.0", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="], + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], @@ -915,7 +915,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "zod": ["zod@4.3.2", "", {}, "sha512-b8L8yn4rIVfiXyHAmnr52/ZEpDumlT0bmxiq3Ws1ybrinhflGpt12Hvv54kYnEsGPRs6o/Ka3/ppA2OWY21IVg=="], + "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], @@ -995,33 +995,43 @@ "@types/estree-jsx/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + "@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], - "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.4", "", {}, "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], - "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + + "@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + + "@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.46.1", "", { "dependencies": { "@typescript-eslint/types": "8.46.1", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA=="], - "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], + "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + + "@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - "@typescript-eslint/typescript-estree/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.46.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/types": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg=="], - "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], "eslint-plugin-react-hooks/@babel/core": ["@babel/core@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.4", "@babel/types": "^7.28.4", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA=="], @@ -1039,7 +1049,7 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.51.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", "@typescript-eslint/scope-manager": "8.51.0", "@typescript-eslint/types": "8.51.0", "@typescript-eslint/typescript-estree": "8.51.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator": ["@babel/generator@7.27.1", "", { "dependencies": { "@babel/parser": "^7.27.1", "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w=="], @@ -1057,11 +1067,15 @@ "@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], - "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], @@ -1083,20 +1097,28 @@ "eslint-plugin-react-hooks/@babel/core/@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.51.0", "", { "dependencies": { "@typescript-eslint/types": "8.51.0", "@typescript-eslint/visitor-keys": "8.51.0" } }, "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA=="], + "typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], - "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.51.0", "", {}, "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag=="], + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="], + + "typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@typescript-eslint/type-utils/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/parser": ["@babel/parser@7.28.3", "", { "dependencies": { "@babel/types": "^7.28.2" }, "bin": "./bin/babel-parser.js" }, "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA=="], "eslint-plugin-react-hooks/@babel/core/@babel/generator/@babel/types": ["@babel/types@7.28.2", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ=="], + "typescript-eslint/@typescript-eslint/utils/@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], "@babel/helper-module-imports/@babel/traverse/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], diff --git a/frontend/package.json b/frontend/package.json index 9d5f4c9..9aebd1e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,14 +30,14 @@ "next-themes": "^0.4.6", "react": "^19.2.3", "react-dom": "^19.2.3", - "react-hook-form": "^7.69.0", - "react-i18next": "^16.5.0", + "react-hook-form": "^7.70.0", + "react-i18next": "^16.5.1", "react-markdown": "^10.1.0", "react-router": "^7.11.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", - "zod": "^4.3.2" + "zod": "^4.3.5" }, "devDependencies": { "@eslint/js": "^9.39.2", @@ -53,7 +53,7 @@ "prettier": "3.7.4", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", - "typescript-eslint": "^8.51.0", - "vite": "^7.3.0" + "typescript-eslint": "^8.52.0", + "vite": "^7.3.1" } } From 1dc83c835ca892dbc78c2f101e42df90eb89e3e7 Mon Sep 17 00:00:00 2001 From: Stavros Date: Wed, 7 Jan 2026 16:30:15 +0200 Subject: [PATCH 06/12] feat: add makefile to simplify development --- .gitignore | 7 +++--- Makefile | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 0eefed8..f505cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # binaries /tinyauth +/tinyauth-arm64 +/tinyauth-amd64 # test docker compose /docker-compose.test* @@ -22,9 +24,6 @@ # tmp directory /tmp -# version files -/internal/assets/version - # data directory /data @@ -36,4 +35,4 @@ /resources # debug files -__debug_* \ No newline at end of file +__debug_* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a7d526e --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +# Go specific stuff +CGO_ENABLED := 0 +GOOS := $(shell go env GOOS) +GOARCH := $(shell go env GOARCH) + +# Build out +TAG_NAME := $(shell git describe --abbrev=0 --exact-match 2> /dev/null || echo "main") +COMMIT_HASH := $(shell git rev-parse HEAD) +BUILD_TIMESTAMP := $(shell date '+%Y-%m-%dT%H:%M:%S') +BIN_NAME := tinyauth-$(GOARCH) + +# Development vars +DEV_COMPOSE := $(shell test -f "docker-compose.test.yml" && echo "docker-compose.test.yml" || echo "docker-compose.yml" ) +PROD_COMPOSE := $(shell test -f "docker-compose.test.prod.yml" && echo "docker-compose.test.prod.yml" || echo "docker-compose.example.yml" ) + +# Deps +deps: + bun install --cwd frontend + go mod download + +# Clean web UI build +clean-webui: + rm -rf internal/assets/dist + rm -rf frontend/dist + +# Build the web UI +webui: clean-webui + bun run --cwd frontend build + cp -r frontend/dist internal/assets + +# Build the binary +binary: webui + CGO_ENABLED=$(CGO_ENABLED) go build -ldflags "-s -w \ + -X tinyauth/internal/config.Version=${TAG_NAME} \ + -X tinyauth/internal/config.CommitHash=${COMMIT_HASH} \ + -X tinyauth/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}" \ + -o ${BIN_NAME} ./cmd/tinyauth + +# Build for amd64 +binary-linux-amd64: + export BIN_NAME=tinyauth-amd64 + export GOARCH=amd64 + export GOOS=linux + $(MAKE) binary + +# Build for arm64 +binary-linux-arm64: + export BIN_NAME=tinyauth-arm64 + export GOARCH=arm64 + export GOOS=linux + $(MAKE) binary + +# Go test +.PHONY: test +test: + go test -v ./... + +# Development +develop: + docker compose -f $(DEV_COMPOSE) up --force-recreate --pull=always --remove-orphans + +# Production +prod: + docker compose -f $(PROD_COMPOSE) up --force-recreate --pull=always --remove-orphans From e3c98faf363fefd4f677197b01730afd6816cb80 Mon Sep 17 00:00:00 2001 From: Pushpinder Singh <53684951+pushpinderbal@users.noreply.github.com> Date: Thu, 8 Jan 2026 03:28:54 -0500 Subject: [PATCH 07/12] fix: username provider appearing when no auth is configured (#568) * feat: allow any HTTP method for /api/auth/envoy and restrict methods for non-envoy proxies * feat: add Allow header for invalid methods in proxyHandler * feat: add session max lifetime and fix refresh logic * fix: set default value for created_at column and improve session expiration logic * fix: correct ldapService reference in authService initialization --------- Co-authored-by: Stavros --- internal/bootstrap/service_bootstrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index 6f6a088..860b3ef 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -67,7 +67,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er LoginTimeout: app.config.Auth.LoginTimeout, LoginMaxRetries: app.config.Auth.LoginMaxRetries, SessionCookieName: app.context.sessionCookieName, - }, dockerService, ldapService, queries) + }, dockerService, services.ldapService, queries) err = authService.Init() From 1ffb838c0f175c228957e58250eb39e5494eb91f Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 8 Jan 2026 15:26:53 +0200 Subject: [PATCH 08/12] feat: add support for global ip filters (#567) --- internal/bootstrap/service_bootstrap.go | 1 + internal/config/config.go | 20 +++++++++++++------- internal/controller/proxy_controller.go | 4 ++-- internal/service/auth_service.go | 13 +++++++++---- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/internal/bootstrap/service_bootstrap.go b/internal/bootstrap/service_bootstrap.go index 860b3ef..02dd453 100644 --- a/internal/bootstrap/service_bootstrap.go +++ b/internal/bootstrap/service_bootstrap.go @@ -67,6 +67,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er LoginTimeout: app.config.Auth.LoginTimeout, LoginMaxRetries: app.config.Auth.LoginMaxRetries, SessionCookieName: app.context.sessionCookieName, + IP: app.config.Auth.IP, }, dockerService, services.ldapService, queries) err = authService.Init() diff --git a/internal/config/config.go b/internal/config/config.go index 2e7af66..61dda81 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -40,13 +40,19 @@ type ServerConfig struct { } type AuthConfig struct { - Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` - UsersFile string `description:"Path to the users file." yaml:"usersFile"` - SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` - SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` - SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"` - LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` - LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` + IP IPConfig `description:"IP whitelisting config options." yaml:"ip"` + Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` + UsersFile string `description:"Path to the users file." yaml:"usersFile"` + SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` + SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` + SessionMaxLifetime int `description:"Maximum session lifetime in seconds." yaml:"sessionMaxLifetime"` + LoginTimeout int `description:"Login timeout in seconds." yaml:"loginTimeout"` + LoginMaxRetries int `description:"Maximum login retries." yaml:"loginMaxRetries"` +} + +type IPConfig struct { + Allow []string `description:"List of allowed IPs or CIDR ranges." yaml:"allow"` + Block []string `description:"List of blocked IPs or CIDR ranges." yaml:"block"` } type OAuthConfig struct { diff --git a/internal/controller/proxy_controller.go b/internal/controller/proxy_controller.go index d6c8beb..2520bfb 100644 --- a/internal/controller/proxy_controller.go +++ b/internal/controller/proxy_controller.go @@ -179,9 +179,9 @@ func (controller *ProxyController) proxyHandler(c *gin.Context) { } if userContext.IsLoggedIn { - appAllowed := controller.auth.IsResourceAllowed(c, userContext, acls) + userAllowed := controller.auth.IsUserAllowed(c, userContext, acls) - if !appAllowed { + if !userAllowed { log.Warn().Str("user", userContext.Username).Str("resource", strings.Split(host, ".")[0]).Msg("User not allowed to access resource") if req.Proxy == "nginx" || !isBrowser { diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index e71fd5f..bde054a 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -35,6 +35,7 @@ type AuthServiceConfig struct { LoginTimeout int LoginMaxRetries int SessionCookieName string + IP config.IPConfig } type AuthService struct { @@ -352,7 +353,7 @@ func (auth *AuthService) UserAuthConfigured() bool { return len(auth.config.Users) > 0 || auth.ldap != nil } -func (auth *AuthService) IsResourceAllowed(c *gin.Context, context config.UserContext, acls config.App) bool { +func (auth *AuthService) IsUserAllowed(c *gin.Context, context config.UserContext, acls config.App) bool { if context.OAuth { log.Debug().Msg("Checking OAuth whitelist") return utils.CheckFilter(acls.OAuth.Whitelist, context.Email) @@ -435,7 +436,11 @@ func (auth *AuthService) GetBasicAuth(c *gin.Context) *config.User { } func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool { - for _, blocked := range acls.Block { + // Merge the global and app IP filter + blockedIps := append(auth.config.IP.Block, acls.Block...) + allowedIPs := append(auth.config.IP.Allow, acls.Allow...) + + for _, blocked := range blockedIps { res, err := utils.FilterIP(blocked, ip) if err != nil { log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list") @@ -447,7 +452,7 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool { } } - for _, allowed := range acls.Allow { + for _, allowed := range allowedIPs { res, err := utils.FilterIP(allowed, ip) if err != nil { log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list") @@ -459,7 +464,7 @@ func (auth *AuthService) CheckIP(acls config.AppIP, ip string) bool { } } - if len(acls.Allow) > 0 { + if len(allowedIPs) > 0 { log.Debug().Str("ip", ip).Msg("IP not in allow list, denying access") return false } From 8872e685895ff8cbd9508e8c8350ad6963d9e358 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:28:50 +0200 Subject: [PATCH 09/12] chore(deps): bump the minor-patch group in /frontend with 2 updates (#569) Bumps the minor-patch group in /frontend with 2 updates: [i18next](https://github.com/i18next/i18next) and [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router). Updates `i18next` from 25.7.3 to 25.7.4 - [Release notes](https://github.com/i18next/i18next/releases) - [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md) - [Commits](https://github.com/i18next/i18next/compare/v25.7.3...v25.7.4) Updates `react-router` from 7.11.0 to 7.12.0 - [Release notes](https://github.com/remix-run/react-router/releases) - [Changelog](https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md) - [Commits](https://github.com/remix-run/react-router/commits/react-router@7.12.0/packages/react-router) --- updated-dependencies: - dependency-name: i18next dependency-version: 25.7.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-patch - dependency-name: react-router dependency-version: 7.12.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: minor-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 8 ++++---- frontend/package.json | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index 6bd74cc..b7997ea 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -16,7 +16,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.7.3", + "i18next": "^25.7.4", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", @@ -27,7 +27,7 @@ "react-hook-form": "^7.70.0", "react-i18next": "^16.5.1", "react-markdown": "^10.1.0", - "react-router": "^7.11.0", + "react-router": "^7.12.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", @@ -589,7 +589,7 @@ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], - "i18next": ["i18next@25.7.3", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA=="], + "i18next": ["i18next@25.7.4", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-hRkpEblXXcXSNbw8mBNq9042OEetgyB/ahc/X17uV/khPwzV+uB8RHceHh3qavyrkPJvmXFKXME2Sy1E0KjAfw=="], "i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="], @@ -807,7 +807,7 @@ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], - "react-router": ["react-router@7.11.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ=="], + "react-router": ["react-router@7.12.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], diff --git a/frontend/package.json b/frontend/package.json index 9aebd1e..e797d57 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -22,7 +22,7 @@ "axios": "^1.13.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "i18next": "^25.7.3", + "i18next": "^25.7.4", "i18next-browser-languagedetector": "^8.2.0", "i18next-resources-to-backend": "^1.2.1", "input-otp": "^1.4.2", @@ -33,7 +33,7 @@ "react-hook-form": "^7.70.0", "react-i18next": "^16.5.1", "react-markdown": "^10.1.0", - "react-router": "^7.11.0", + "react-router": "^7.12.0", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tailwindcss": "^4.1.18", From 0aa8037edccf36659bf300fed0482fa177af0108 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 15:30:01 +0200 Subject: [PATCH 10/12] chore(deps-dev): bump globals from 16.5.0 to 17.0.0 in /frontend (#570) Bumps [globals](https://github.com/sindresorhus/globals) from 16.5.0 to 17.0.0. - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v16.5.0...v17.0.0) --- updated-dependencies: - dependency-name: globals dependency-version: 17.0.0 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/bun.lock | 4 ++-- frontend/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/bun.lock b/frontend/bun.lock index b7997ea..c5f1241 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -43,7 +43,7 @@ "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", - "globals": "^16.5.0", + "globals": "^17.0.0", "prettier": "3.7.4", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", @@ -563,7 +563,7 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], - "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], + "globals": ["globals@17.0.0", "", {}, "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], diff --git a/frontend/package.json b/frontend/package.json index e797d57..d3556b6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -49,7 +49,7 @@ "eslint": "^9.39.2", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.26", - "globals": "^16.5.0", + "globals": "^17.0.0", "prettier": "3.7.4", "tw-animate-css": "^1.4.0", "typescript": "~5.9.3", From 454612226b15be3c703270c50b86ac7cacc00a29 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 8 Jan 2026 15:35:58 +0200 Subject: [PATCH 11/12] chore: move sql files to sql directory --- internal/repository/{query.sql.go => queries.sql.go} | 2 +- query.sql => sql/queries.sql | 0 schema.sql => sql/schema.sql | 0 sqlc.yml | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) rename internal/repository/{query.sql.go => queries.sql.go} (99%) rename query.sql => sql/queries.sql (100%) rename schema.sql => sql/schema.sql (100%) diff --git a/internal/repository/query.sql.go b/internal/repository/queries.sql.go similarity index 99% rename from internal/repository/query.sql.go rename to internal/repository/queries.sql.go index 5924842..e171b7a 100644 --- a/internal/repository/query.sql.go +++ b/internal/repository/queries.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 -// source: query.sql +// source: queries.sql package repository diff --git a/query.sql b/sql/queries.sql similarity index 100% rename from query.sql rename to sql/queries.sql diff --git a/schema.sql b/sql/schema.sql similarity index 100% rename from schema.sql rename to sql/schema.sql diff --git a/sqlc.yml b/sqlc.yml index bced79b..77b3a71 100644 --- a/sqlc.yml +++ b/sqlc.yml @@ -1,8 +1,8 @@ version: "2" sql: - engine: "sqlite" - queries: "query.sql" - schema: "schema.sql" + queries: "sql/queries.sql" + schema: "sql/schema.sql" gen: go: package: "repository" From e3f92ce4fcc455e4057e06b6aef107f615e6ef57 Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 8 Jan 2026 16:03:37 +0200 Subject: [PATCH 12/12] refactor: simplify user parsing (#571) --- internal/bootstrap/router_bootstrap.go | 3 +- internal/config/config.go | 12 +-- internal/controller/proxy_controller_test.go | 2 +- internal/controller/user_controller_test.go | 2 +- internal/service/auth_service.go | 4 +- internal/utils/user_utils.go | 83 ++++++++++---------- internal/utils/user_utils_test.go | 14 ++-- 7 files changed, 59 insertions(+), 61 deletions(-) diff --git a/internal/bootstrap/router_bootstrap.go b/internal/bootstrap/router_bootstrap.go index 531f5e0..abcd81b 100644 --- a/internal/bootstrap/router_bootstrap.go +++ b/internal/bootstrap/router_bootstrap.go @@ -2,7 +2,6 @@ package bootstrap import ( "fmt" - "strings" "github.com/steveiliop56/tinyauth/internal/controller" "github.com/steveiliop56/tinyauth/internal/middleware" @@ -15,7 +14,7 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) { engine.Use(gin.Recovery()) if len(app.config.Server.TrustedProxies) > 0 { - err := engine.SetTrustedProxies(strings.Split(app.config.Server.TrustedProxies, ",")) + err := engine.SetTrustedProxies(app.config.Server.TrustedProxies) if err != nil { return nil, fmt.Errorf("failed to set trusted proxies: %w", err) diff --git a/internal/config/config.go b/internal/config/config.go index 61dda81..920288d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -33,15 +33,15 @@ type Config struct { } type ServerConfig struct { - Port int `description:"The port on which the server listens." yaml:"port"` - Address string `description:"The address on which the server listens." yaml:"address"` - SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` - TrustedProxies string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"` + Port int `description:"The port on which the server listens." yaml:"port"` + Address string `description:"The address on which the server listens." yaml:"address"` + SocketPath string `description:"The path to the Unix socket." yaml:"socketPath"` + TrustedProxies []string `description:"Comma-separated list of trusted proxy addresses." yaml:"trustedProxies"` } type AuthConfig struct { IP IPConfig `description:"IP whitelisting config options." yaml:"ip"` - Users string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` + Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"` UsersFile string `description:"Path to the users file." yaml:"usersFile"` SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"` SessionExpiry int `description:"Session expiry time in seconds." yaml:"sessionExpiry"` @@ -56,7 +56,7 @@ type IPConfig struct { } type OAuthConfig struct { - Whitelist string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` + Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` } diff --git a/internal/controller/proxy_controller_test.go b/internal/controller/proxy_controller_test.go index 5f44382..57711fc 100644 --- a/internal/controller/proxy_controller_test.go +++ b/internal/controller/proxy_controller_test.go @@ -57,7 +57,7 @@ func setupProxyController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.En Password: "$2a$10$ne6z693sTgzT3ePoQ05PgOecUHnBjM7sSNj6M.l5CLUP.f6NyCnt.", // test }, }, - OauthWhitelist: "", + OauthWhitelist: []string{}, SessionExpiry: 3600, SessionMaxLifetime: 0, SecureCookie: false, diff --git a/internal/controller/user_controller_test.go b/internal/controller/user_controller_test.go index dc8ec5c..99768c7 100644 --- a/internal/controller/user_controller_test.go +++ b/internal/controller/user_controller_test.go @@ -60,7 +60,7 @@ func setupUserController(t *testing.T, middlewares *[]gin.HandlerFunc) (*gin.Eng TotpSecret: totpSecret, }, }, - OauthWhitelist: "", + OauthWhitelist: []string{}, SessionExpiry: 3600, SessionMaxLifetime: 0, SecureCookie: false, diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index bde054a..6d829c6 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -27,7 +27,7 @@ type LoginAttempt struct { type AuthServiceConfig struct { Users []config.User - OauthWhitelist string + OauthWhitelist []string SessionExpiry int SessionMaxLifetime int SecureCookie bool @@ -187,7 +187,7 @@ func (auth *AuthService) RecordLoginAttempt(identifier string, success bool) { } func (auth *AuthService) IsEmailWhitelisted(email string) bool { - return utils.CheckFilter(auth.config.OauthWhitelist, email) + return utils.CheckFilter(strings.Join(auth.config.OauthWhitelist, ","), email) } func (auth *AuthService) CreateSessionCookie(c *gin.Context, data *config.SessionCookie) error { diff --git a/internal/utils/user_utils.go b/internal/utils/user_utils.go index 8ee8e94..a56bd6e 100644 --- a/internal/utils/user_utils.go +++ b/internal/utils/user_utils.go @@ -7,22 +7,14 @@ import ( "github.com/steveiliop56/tinyauth/internal/config" ) -func ParseUsers(users string) ([]config.User, error) { - var usersParsed []config.User +func ParseUsers(usersStr []string) ([]config.User, error) { + var users []config.User - users = strings.TrimSpace(users) - - if users == "" { + if len(usersStr) == 0 { return []config.User{}, nil } - userList := strings.Split(users, ",") - - if len(userList) == 0 { - return []config.User{}, errors.New("invalid user format") - } - - for _, user := range userList { + for _, user := range usersStr { if strings.TrimSpace(user) == "" { continue } @@ -30,64 +22,71 @@ func ParseUsers(users string) ([]config.User, error) { if err != nil { return []config.User{}, err } - usersParsed = append(usersParsed, parsed) + users = append(users, parsed) } - return usersParsed, nil + return users, nil } -func GetUsers(conf string, file string) ([]config.User, error) { - var users string +func GetUsers(usersCfg []string, usersPath string) ([]config.User, error) { + var usersStr []string - if conf == "" && file == "" { + if len(usersCfg) == 0 && usersPath == "" { return []config.User{}, nil } - if conf != "" { - users += conf + if len(usersCfg) > 0 { + usersStr = append(usersStr, usersCfg...) } - if file != "" { - contents, err := ReadFile(file) + if usersPath != "" { + contents, err := ReadFile(usersPath) + if err != nil { return []config.User{}, err } - if users != "" { - users += "," + + lines := strings.SplitSeq(contents, "\n") + + for line := range lines { + lineTrimmed := strings.TrimSpace(line) + if lineTrimmed == "" { + continue + } + usersStr = append(usersStr, lineTrimmed) } - users += ParseFileToLine(contents) } - return ParseUsers(users) + return ParseUsers(usersStr) } -func ParseUser(user string) (config.User, error) { - if strings.Contains(user, "$$") { - user = strings.ReplaceAll(user, "$$", "$") +func ParseUser(userStr string) (config.User, error) { + if strings.Contains(userStr, "$$") { + userStr = strings.ReplaceAll(userStr, "$$", "$") } - userSplit := strings.Split(user, ":") + parts := strings.SplitN(userStr, ":", 4) - if len(userSplit) < 2 || len(userSplit) > 3 { + if len(parts) < 2 || len(parts) > 3 { return config.User{}, errors.New("invalid user format") } - for _, userPart := range userSplit { - if strings.TrimSpace(userPart) == "" { + for i, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed == "" { return config.User{}, errors.New("invalid user format") } + parts[i] = trimmed } - if len(userSplit) == 2 { - return config.User{ - Username: strings.TrimSpace(userSplit[0]), - Password: strings.TrimSpace(userSplit[1]), - }, nil + user := config.User{ + Username: parts[0], + Password: parts[1], } - return config.User{ - Username: strings.TrimSpace(userSplit[0]), - Password: strings.TrimSpace(userSplit[1]), - TotpSecret: strings.TrimSpace(userSplit[2]), - }, nil + if len(parts) == 3 { + user.TotpSecret = parts[2] + } + + return user, nil } diff --git a/internal/utils/user_utils_test.go b/internal/utils/user_utils_test.go index 3cc23c3..658fbdc 100644 --- a/internal/utils/user_utils_test.go +++ b/internal/utils/user_utils_test.go @@ -22,7 +22,7 @@ func TestGetUsers(t *testing.T) { defer os.Remove("/tmp/tinyauth_users_test.txt") // Test file - users, err := utils.GetUsers("", "/tmp/tinyauth_users_test.txt") + users, err := utils.GetUsers([]string{}, "/tmp/tinyauth_users_test.txt") assert.NilError(t, err) @@ -34,7 +34,7 @@ func TestGetUsers(t *testing.T) { assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) // Test config - users, err = utils.GetUsers("user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "") + users, err = utils.GetUsers([]string{"user3:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user4:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "") assert.NilError(t, err) @@ -46,7 +46,7 @@ func TestGetUsers(t *testing.T) { assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[1].Password) // Test both - users, err = utils.GetUsers("user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "/tmp/tinyauth_users_test.txt") + users, err = utils.GetUsers([]string{"user5:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G"}, "/tmp/tinyauth_users_test.txt") assert.NilError(t, err) @@ -60,14 +60,14 @@ func TestGetUsers(t *testing.T) { assert.Equal(t, "$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", users[2].Password) // Test empty - users, err = utils.GetUsers("", "") + users, err = utils.GetUsers([]string{}, "") assert.NilError(t, err) assert.Equal(t, 0, len(users)) // Test non-existent file - users, err = utils.GetUsers("", "/tmp/non_existent_file.txt") + users, err = utils.GetUsers([]string{}, "/tmp/non_existent_file.txt") assert.ErrorContains(t, err, "no such file or directory") @@ -76,7 +76,7 @@ func TestGetUsers(t *testing.T) { func TestParseUsers(t *testing.T) { // Valid users - users, err := utils.ParseUsers("user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G,user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF") // user2 has TOTP + users, err := utils.ParseUsers([]string{"user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G", "user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF"}) // user2 has TOTP assert.NilError(t, err) @@ -90,7 +90,7 @@ func TestParseUsers(t *testing.T) { assert.Equal(t, "ABCDEF", users[1].TotpSecret) // Valid weirdly spaced users - users, err = utils.ParseUsers(" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G , user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF ") // Spacing is on purpose + users, err = utils.ParseUsers([]string{" user1:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G ", " user2:$2a$10$Mz5xhkfSJUtPWkzCd/TdaePh9CaXc5QcGII5wIMPLSR46eTwma30G:ABCDEF "}) // Spacing is on purpose assert.NilError(t, err) assert.Equal(t, 2, len(users))