Feat v0.6.9 session lifetime config
- Admin-configurable session TTLs (access: 5m–60m, refresh: 1h–90d) stored in global_settings under "session" key - "Keep me logged in" checkbox on login form; unchecked caps refresh token at 24h, checked uses full admin-configured TTL - generateTokens() reads config instead of hardcoded 15m/7d; expires_in and refresh_expires_in in JSON response reflect actual TTLs - Cookie max-age tracks chosen refresh lifetime (not hardcoded 604800) - Optional idle timeout: admin toggle + configurable duration; server rejects refresh if last_activity_at exceeds threshold - Client SDK activity ping (POST /auth/activity, debounced 1/min) - Admin Settings > Session section with dropdowns for all three knobs - 7 new unit tests for duration parsing, clamping, and defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -148,9 +148,10 @@ func (s *UserStore) ListActiveUserIDs(ctx context.Context) ([]string, error) {
|
||||
|
||||
// ── Refresh Tokens ──────────────────────────
|
||||
|
||||
func (s *UserStore) CreateRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time) error {
|
||||
_, err := DB.ExecContext(ctx, `INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at) VALUES (?, ?, ?, ?)`,
|
||||
store.NewID(), userID, tokenHash, expiresAt.Format(timeFmt))
|
||||
func (s *UserStore) CreateRefreshToken(ctx context.Context, userID, tokenHash string, expiresAt time.Time, keepLogin bool) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
`INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at, keep_login, last_activity_at) VALUES (?, ?, ?, ?, ?, datetime('now'))`,
|
||||
store.NewID(), userID, tokenHash, expiresAt.Format(timeFmt), keepLogin)
|
||||
return err
|
||||
}
|
||||
func (s *UserStore) GetRefreshToken(ctx context.Context, tokenHash string) (string, error) {
|
||||
@@ -158,6 +159,23 @@ func (s *UserStore) GetRefreshToken(ctx context.Context, tokenHash string) (stri
|
||||
err := DB.QueryRowContext(ctx, `SELECT user_id FROM refresh_tokens WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > datetime('now')`, tokenHash).Scan(&userID)
|
||||
return userID, err
|
||||
}
|
||||
func (s *UserStore) GetRefreshTokenInfo(ctx context.Context, tokenHash string) (*store.RefreshTokenInfo, error) {
|
||||
var info store.RefreshTokenInfo
|
||||
var lastAct *string
|
||||
err := DB.QueryRowContext(ctx,
|
||||
`SELECT user_id, COALESCE(keep_login, 0), last_activity_at
|
||||
FROM refresh_tokens WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > datetime('now')`,
|
||||
tokenHash).Scan(&info.UserID, &info.KeepLogin, &lastAct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lastAct != nil {
|
||||
if t, err := time.Parse(timeFmt, *lastAct); err == nil {
|
||||
info.LastActivityAt = &t
|
||||
}
|
||||
}
|
||||
return &info, nil
|
||||
}
|
||||
func (s *UserStore) RevokeRefreshToken(ctx context.Context, tokenHash string) error {
|
||||
_, err := DB.ExecContext(ctx, "UPDATE refresh_tokens SET revoked_at = datetime('now') WHERE token_hash = ?", tokenHash)
|
||||
return err
|
||||
@@ -170,6 +188,12 @@ func (s *UserStore) CleanExpiredTokens(ctx context.Context) error {
|
||||
_, err := DB.ExecContext(ctx, "DELETE FROM refresh_tokens WHERE expires_at < datetime('now', '-30 days')")
|
||||
return err
|
||||
}
|
||||
func (s *UserStore) UpdateRefreshTokenActivity(ctx context.Context, userID string) error {
|
||||
_, err := DB.ExecContext(ctx,
|
||||
"UPDATE refresh_tokens SET last_activity_at = datetime('now') WHERE user_id = ? AND revoked_at IS NULL",
|
||||
userID)
|
||||
return err
|
||||
}
|
||||
|
||||
// ── Internal ────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user