AbstractLoginFailureTracker.kt

package io.github.lishangbu.avalon.oauth2.authorizationserver.login

import io.github.lishangbu.avalon.oauth2.common.properties.Oauth2Properties
import java.time.Clock
import java.time.Duration
import java.time.Instant

data class LoginFailureState(
    val failures: Int,
    val lockUntil: Instant? = null,
)

/**
 * Shared login failure tracker configuration and state transition helpers.
 */
abstract class AbstractLoginFailureTracker(
    properties: Oauth2Properties?,
    private val clock: Clock = Clock.systemUTC(),
) : LoginFailureTracker {
    protected val maxFailures: Int = (properties?.maxLoginFailures ?: 0).coerceAtLeast(0)
    protected val lockDuration: Duration? =
        properties
            ?.getLoginLockDuration()
            ?.takeIf { !it.isZero && !it.isNegative }

    override fun isEnabled(): Boolean = maxFailures > 0 && lockDuration != null

    protected fun normalize(username: String?): String? = username?.trim()?.takeIf { it.isNotEmpty() }

    protected fun now(): Instant = Instant.now(clock)

    protected fun getRemainingLock(
        state: LoginFailureState?,
        now: Instant = now(),
    ): Duration? {
        val lockUntil = state?.lockUntil ?: return null
        if (!lockUntil.isAfter(now)) {
            return null
        }
        return Duration.between(now, lockUntil)
    }

    protected fun nextState(
        current: LoginFailureState?,
        now: Instant = now(),
    ): LoginFailureState {
        if (getRemainingLock(current, now) != null) {
            return checkNotNull(current)
        }

        val nextFailures =
            if (current?.lockUntil != null) {
                1
            } else {
                (current?.failures ?: 0) + 1
            }

        val effectiveLockDuration =
            checkNotNull(lockDuration) {
                "Login failure tracking is disabled."
            }

        if (nextFailures >= maxFailures) {
            return LoginFailureState(
                failures = 0,
                lockUntil = now.plus(effectiveLockDuration),
            )
        }
        return LoginFailureState(failures = nextFailures)
    }
}