228 lines
6.1 KiB
Go
228 lines
6.1 KiB
Go
package tokenmanager
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/maplefile/client"
|
|
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/service/auth"
|
|
"codeberg.org/mapleopentech/monorepo/native/desktop/maplefile/internal/usecase/session"
|
|
)
|
|
|
|
// Manager handles automatic token refresh with graceful shutdown
|
|
type Manager struct {
|
|
config Config
|
|
client *client.Client
|
|
authService *auth.Service
|
|
getSession *session.GetByIdUseCase
|
|
logger *zap.Logger
|
|
|
|
// Lifecycle management
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
stopCh chan struct{} // Signal to stop
|
|
stoppedCh chan struct{} // Confirmation of stopped
|
|
running atomic.Bool // Thread-safe running flag
|
|
|
|
// Refresh state management
|
|
mu sync.Mutex
|
|
consecutiveFailures int
|
|
}
|
|
|
|
// New creates a new token manager
|
|
func New(
|
|
config Config,
|
|
client *client.Client,
|
|
authService *auth.Service,
|
|
getSession *session.GetByIdUseCase,
|
|
logger *zap.Logger,
|
|
) *Manager {
|
|
return &Manager{
|
|
config: config,
|
|
client: client,
|
|
authService: authService,
|
|
getSession: getSession,
|
|
logger: logger.Named("token-manager"),
|
|
}
|
|
}
|
|
|
|
// Start begins the token refresh background process
|
|
// Safe to call multiple times - will only start once
|
|
func (m *Manager) Start() {
|
|
// Only start if not already running
|
|
if !m.running.CompareAndSwap(false, true) {
|
|
m.logger.Debug("Token manager already running, skipping start")
|
|
return
|
|
}
|
|
|
|
m.ctx, m.cancel = context.WithCancel(context.Background())
|
|
m.stopCh = make(chan struct{})
|
|
m.stoppedCh = make(chan struct{})
|
|
m.consecutiveFailures = 0
|
|
|
|
m.logger.Info("Token manager starting")
|
|
go m.refreshLoop()
|
|
}
|
|
|
|
// Stop gracefully stops the token refresh background process
|
|
// Blocks until stopped or context deadline exceeded
|
|
func (m *Manager) Stop(ctx context.Context) error {
|
|
if !m.running.Load() {
|
|
m.logger.Debug("Token manager not running, nothing to stop")
|
|
return nil
|
|
}
|
|
|
|
m.logger.Info("Token manager stopping...")
|
|
|
|
// Signal stop
|
|
close(m.stopCh)
|
|
|
|
// Wait for goroutine to finish or timeout
|
|
select {
|
|
case <-m.stoppedCh:
|
|
m.logger.Info("Token manager stopped gracefully")
|
|
return nil
|
|
case <-ctx.Done():
|
|
m.logger.Warn("Token manager stop timeout, forcing cancellation")
|
|
m.cancel()
|
|
// Wait a bit more for cancellation to take effect
|
|
select {
|
|
case <-m.stoppedCh:
|
|
m.logger.Info("Token manager stopped after forced cancellation")
|
|
return nil
|
|
case <-time.After(100 * time.Millisecond):
|
|
m.logger.Error("Token manager failed to stop cleanly")
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
}
|
|
|
|
// IsRunning returns true if the token manager is currently running
|
|
func (m *Manager) IsRunning() bool {
|
|
return m.running.Load()
|
|
}
|
|
|
|
// refreshLoop is the background goroutine that checks and refreshes tokens
|
|
func (m *Manager) refreshLoop() {
|
|
defer close(m.stoppedCh)
|
|
defer m.running.Store(false)
|
|
defer m.logger.Info("Token refresh loop exited")
|
|
|
|
ticker := time.NewTicker(m.config.CheckInterval)
|
|
defer ticker.Stop()
|
|
|
|
m.logger.Info("Token refresh loop started",
|
|
zap.Duration("check_interval", m.config.CheckInterval),
|
|
zap.Duration("refresh_before_expiry", m.config.RefreshBeforeExpiry))
|
|
|
|
// Do initial check immediately
|
|
if err := m.checkAndRefresh(); err != nil {
|
|
m.logger.Error("Initial token refresh check failed", zap.Error(err))
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-m.stopCh:
|
|
m.logger.Info("Token refresh loop received stop signal")
|
|
return
|
|
|
|
case <-m.ctx.Done():
|
|
m.logger.Info("Token refresh loop context cancelled")
|
|
return
|
|
|
|
case <-ticker.C:
|
|
if err := m.checkAndRefresh(); err != nil {
|
|
m.logger.Error("Token refresh check failed", zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// checkAndRefresh checks if token refresh is needed and performs it
|
|
func (m *Manager) checkAndRefresh() error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
// Get current session
|
|
sess, err := m.getSession.Execute()
|
|
if err != nil {
|
|
m.logger.Debug("No session found, skipping refresh check", zap.Error(err))
|
|
return nil // Not an error - user might not be logged in
|
|
}
|
|
|
|
if sess == nil {
|
|
m.logger.Debug("Session is nil, skipping refresh check")
|
|
return nil
|
|
}
|
|
|
|
// Check if session is still valid
|
|
if sess.IsExpired() {
|
|
m.logger.Warn("Session has expired, forcing logout")
|
|
return m.forceLogout()
|
|
}
|
|
|
|
// Check if refresh is needed
|
|
timeUntilExpiry := time.Until(sess.ExpiresAt)
|
|
if timeUntilExpiry > m.config.RefreshBeforeExpiry {
|
|
// No refresh needed yet
|
|
if m.consecutiveFailures > 0 {
|
|
// Reset failure counter on successful check
|
|
m.logger.Info("Session valid, resetting failure counter")
|
|
m.consecutiveFailures = 0
|
|
}
|
|
m.logger.Debug("Token refresh not needed yet",
|
|
zap.Duration("time_until_expiry", timeUntilExpiry))
|
|
return nil
|
|
}
|
|
|
|
// Refresh needed
|
|
m.logger.Info("Token refresh needed",
|
|
zap.Duration("time_until_expiry", timeUntilExpiry))
|
|
|
|
// Attempt refresh (with background context, not the manager's context)
|
|
refreshCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
if err := m.client.RefreshToken(refreshCtx); err != nil {
|
|
m.consecutiveFailures++
|
|
m.logger.Error("Token refresh failed",
|
|
zap.Error(err),
|
|
zap.Int("consecutive_failures", m.consecutiveFailures),
|
|
zap.Int("max_failures", m.config.MaxConsecutiveFailures))
|
|
|
|
if m.consecutiveFailures >= m.config.MaxConsecutiveFailures {
|
|
m.logger.Error("Max consecutive refresh failures reached, forcing logout")
|
|
return m.forceLogout()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Success - reset failure counter
|
|
m.consecutiveFailures = 0
|
|
m.logger.Info("Token refreshed successfully",
|
|
zap.Duration("time_until_old_expiry", timeUntilExpiry))
|
|
|
|
return nil
|
|
}
|
|
|
|
// forceLogout forces a logout due to refresh failures
|
|
func (m *Manager) forceLogout() error {
|
|
m.logger.Warn("Forcing logout due to token refresh issues")
|
|
|
|
// Use background context since manager might be shutting down
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
if err := m.authService.Logout(ctx); err != nil {
|
|
m.logger.Error("Failed to force logout", zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
m.logger.Info("Force logout completed successfully")
|
|
return nil
|
|
}
|