refactor: move auth, GDPR, and booking cleanup to centralized scheduler

Convert CleanupRevokedJTIs to return (int, error) and remove StartJTICleanup goroutine. Add CleanupStaleLoginEntries and CleanupGDPRExportCache for centralized scheduler. Add clock.London timezone location.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-07-07 00:10:00 +01:00
co-authored by Sisyphus
parent a4fa75154d
commit e86248b27c
7 changed files with 276 additions and 79 deletions
+119
View File
@@ -1847,5 +1847,124 @@ func TestRefreshToken_RevokesOldJTI_DBBacked(t *testing.T) {
}
}
// ============================================================
// CleanupStaleLoginEntries Tests
// ============================================================
func TestCleanupStaleLoginEntries_Empty(t *testing.T) {
t.Parallel()
loginStateMu.Lock()
saved := loginInProgress
loginInProgress = make(map[string]time.Time)
loginStateMu.Unlock()
defer func() {
loginStateMu.Lock()
loginInProgress = saved
loginStateMu.Unlock()
}()
_, err := CleanupStaleLoginEntries(context.Background())
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
}
func TestCleanupStaleLoginEntries_RemovesStale(t *testing.T) {
t.Parallel()
loginStateMu.Lock()
saved := loginInProgress
loginInProgress = map[string]time.Time{
"stale-user": clock.Now().Add(-60 * time.Second),
}
loginStateMu.Unlock()
defer func() {
loginStateMu.Lock()
loginInProgress = saved
loginStateMu.Unlock()
}()
_, err := CleanupStaleLoginEntries(context.Background())
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
loginStateMu.Lock()
_, exists := loginInProgress["stale-user"]
deleted := loginInProgress["stale-user"]
loginStateMu.Unlock()
if exists {
t.Errorf("expected stale entry (60s old) to be removed, got %v", deleted)
}
}
func TestCleanupStaleLoginEntries_PreservesRecent(t *testing.T) {
t.Parallel()
loginStateMu.Lock()
saved := loginInProgress
loginInProgress = map[string]time.Time{
"recent-user": clock.Now().Add(-5 * time.Second),
}
loginStateMu.Unlock()
defer func() {
loginStateMu.Lock()
loginInProgress = saved
loginStateMu.Unlock()
}()
_, err := CleanupStaleLoginEntries(context.Background())
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
loginStateMu.Lock()
_, exists := loginInProgress["recent-user"]
loginStateMu.Unlock()
if !exists {
t.Error("expected recent entry (5s old) to be preserved")
}
}
func TestCleanupStaleLoginEntries_Mixed(t *testing.T) {
t.Parallel()
loginStateMu.Lock()
saved := loginInProgress
loginInProgress = map[string]time.Time{
"stale-user": clock.Now().Add(-60 * time.Second),
"recent-user": clock.Now().Add(-5 * time.Second),
"borderline": clock.Now().Add(-29 * time.Second), // Just under 30s threshold
}
loginStateMu.Unlock()
defer func() {
loginStateMu.Lock()
loginInProgress = saved
loginStateMu.Unlock()
}()
_, err := CleanupStaleLoginEntries(context.Background())
if err != nil {
t.Errorf("expected nil error, got %v", err)
}
loginStateMu.Lock()
_, staleExists := loginInProgress["stale-user"]
_, recentExists := loginInProgress["recent-user"]
_, borderlineExists := loginInProgress["borderline"]
loginStateMu.Unlock()
if staleExists {
t.Error("expected stale-user (60s old) to be removed")
}
if !recentExists {
t.Error("expected recent-user (5s old) to be preserved")
}
if !borderlineExists {
t.Error("expected borderline entry (29s old) to be preserved")
}
}
// Ensure test compilation - import pgxpool to avoid unused import
var _ = func() *pgxpool.Pool { return nil }
+16 -27
View File
@@ -1,10 +1,10 @@
package auth
import (
"context"
"crussell/auth"
"crussell/clock"
"crussell/db"
"github.com/jackc/pgx/v5"
"crussell/internal/dav"
"crussell/internal/validators"
"crussell/internal/zxcvbnjs"
@@ -16,21 +16,22 @@ import (
"log"
"net/http"
"github.com/go-chi/chi/v5/middleware"
"github.com/jackc/pgx/v5"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/go-chi/chi/v5/middleware"
"github.com/nyaruka/phonenumbers"
"golang.org/x/crypto/bcrypt"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const maxLoginInProgress = 20
// Login state management
@@ -39,30 +40,18 @@ var (
loginInProgress = make(map[string]time.Time)
)
func init() {
go func() {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
for range ticker.C {
func() {
defer func() {
if r := recover(); r != nil {
log.Printf("Panic recovered in login state cleanup ticker: %v", r)
}
}()
loginStateMu.Lock()
now := clock.Now()
// Clean up stuck loginInProgress entries (older than 30s)
for userID, startedAt := range loginInProgress {
if now.Sub(startedAt) > 30*time.Second {
delete(loginInProgress, userID)
}
}
loginStateMu.Unlock()
}()
// CleanupStaleLoginEntries removes stuck loginInProgress entries older than 30 seconds.
// Called by the centralised jobs scheduler.
func CleanupStaleLoginEntries(ctx context.Context) (int, error) {
loginStateMu.Lock()
defer loginStateMu.Unlock()
now := clock.Now()
for userID, startedAt := range loginInProgress {
if now.Sub(startedAt) > 30*time.Second {
delete(loginInProgress, userID)
}
}()
}
return 0, nil
}
type RegisterRequest struct {