1a9c518f96
Provide TOTP-authenticated, role-controlled user and credit workflows with paginated audit data and SQL-backed statistics so operations can manage growth safely.
610 lines
26 KiB
Kotlin
610 lines
26 KiB
Kotlin
package com.osglab.account
|
|
|
|
import com.osglab.account.common.api.installApiStatusPages
|
|
import com.osglab.account.common.security.FieldEncryptor
|
|
import com.osglab.account.common.security.IdentityFingerprint
|
|
import com.osglab.account.common.security.SessionJwt
|
|
import com.osglab.account.common.security.installSessionAuthentication
|
|
import com.osglab.account.config.AppConfig
|
|
import com.osglab.account.config.DatabaseFactory
|
|
import com.osglab.account.features.admin.grants.services.AdminGrantService
|
|
import com.osglab.account.features.admin.repositories.AdminRepository
|
|
import com.osglab.account.features.admin.repositories.ExposedAdminRepository
|
|
import com.osglab.account.features.admin.routes.adminApiRoutes
|
|
import com.osglab.account.features.admin.routes.adminWebRoutes
|
|
import com.osglab.account.features.admin.security.AdminPasswordHasher
|
|
import com.osglab.account.features.admin.security.AdminTotpVerifier
|
|
import com.osglab.account.features.admin.security.BouncyCastleArgon2idPasswordHasher
|
|
import com.osglab.account.features.admin.security.HmacTotpVerifier
|
|
import com.osglab.account.features.admin.services.AdminAuthService
|
|
import com.osglab.account.features.admin.services.AdminAuditService
|
|
import com.osglab.account.features.admin.services.AdminBootstrapConfig
|
|
import com.osglab.account.features.admin.services.AdminBootstrapService
|
|
import com.osglab.account.features.admin.services.AdminOperatorService
|
|
import com.osglab.account.features.admin.services.AdminSessionService
|
|
import com.osglab.account.features.admin.stats.repositories.AdminStatsRepository
|
|
import com.osglab.account.features.admin.stats.repositories.ExposedAdminStatsRepository
|
|
import com.osglab.account.features.admin.stats.services.AdminStatsService
|
|
import com.osglab.account.features.admin.users.repositories.AdminUsersRepository
|
|
import com.osglab.account.features.admin.users.repositories.ExposedAdminUsersRepository
|
|
import com.osglab.account.features.admin.users.services.AdminUsersService
|
|
import com.osglab.account.features.account.AccountRepository
|
|
import com.osglab.account.features.account.AccountReauthenticator
|
|
import com.osglab.account.features.account.AccountService
|
|
import com.osglab.account.features.account.AppleAccountReauthenticator
|
|
import com.osglab.account.features.account.AppleRevocationOutboxProcessor
|
|
import com.osglab.account.features.account.ExposedAccountRepository
|
|
import com.osglab.account.features.account.accountRoutes
|
|
import com.osglab.account.features.appleevents.AppleEventService
|
|
import com.osglab.account.features.appleevents.AppleEventRepository
|
|
import com.osglab.account.features.appleevents.AppleEventVerifier
|
|
import com.osglab.account.features.appleevents.ExposedAppleEventRepository
|
|
import com.osglab.account.features.appleevents.appleEventRoutes
|
|
import com.osglab.account.features.auth.AccountProvisioner
|
|
import com.osglab.account.features.auth.AppleIdentityTokenVerifier
|
|
import com.osglab.account.features.auth.AppleJwksProvider
|
|
import com.osglab.account.features.auth.AppleTokenClient
|
|
import com.osglab.account.features.auth.AuthRepository
|
|
import com.osglab.account.features.auth.ExposedAuthRepository
|
|
import com.osglab.account.features.auth.RemoteAppleJwksProvider
|
|
import com.osglab.account.features.auth.SessionService
|
|
import com.osglab.account.features.auth.SessionAccessAuthenticator
|
|
import com.osglab.account.features.auth.authRoutes
|
|
import com.osglab.account.features.auth.createAppleTokenClient
|
|
import com.osglab.account.features.credits.repositories.BillingTransactionRunner
|
|
import com.osglab.account.features.credits.repositories.ExposedBillingTransactionRunner
|
|
import com.osglab.account.features.credits.routes.AuthenticatedUserExtractor
|
|
import com.osglab.account.features.credits.routes.creditRoutes
|
|
import com.osglab.account.features.credits.services.CreditOperations
|
|
import com.osglab.account.features.credits.services.CreditService
|
|
import com.osglab.account.features.credits.services.ReferralRewardConfig
|
|
import com.osglab.account.features.gateway.adapters.CreditReservationAdapter
|
|
import com.osglab.account.features.gateway.adapters.SessionIdentityAdapter
|
|
import com.osglab.account.features.gateway.GatewaySettings
|
|
import com.osglab.account.features.gateway.asr.AsrStreamingService
|
|
import com.osglab.account.features.gateway.ports.CreditReservationPort
|
|
import com.osglab.account.features.gateway.ports.GatewayAccessTokenPort
|
|
import com.osglab.account.features.gateway.ports.GatewayGrantPort
|
|
import com.osglab.account.features.gateway.ports.GatewayGrantRepository
|
|
import com.osglab.account.features.gateway.ports.GatewayIdentityPort
|
|
import com.osglab.account.features.gateway.ports.GatewayUsagePort
|
|
import com.osglab.account.features.gateway.providers.GatewayProvider
|
|
import com.osglab.account.features.gateway.providers.ProviderCatalog
|
|
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekConfig
|
|
import com.osglab.account.features.gateway.providers.deepseek.DeepSeekProvider
|
|
import com.osglab.account.features.gateway.providers.volcengine.KtorVolcengineAsrTransport
|
|
import com.osglab.account.features.gateway.providers.volcengine.VolcengineAsrConfig
|
|
import com.osglab.account.features.gateway.providers.volcengine.VolcengineAsrProvider
|
|
import com.osglab.account.features.gateway.repositories.ExposedGatewayRepository
|
|
import com.osglab.account.features.gateway.routes.configureGatewayRoutes
|
|
import com.osglab.account.features.gateway.services.GatewayBearerIdentity
|
|
import com.osglab.account.features.gateway.services.GatewayGrantService
|
|
import com.osglab.account.features.gateway.services.GatewayReconciliationService
|
|
import com.osglab.account.features.gateway.services.GatewayService
|
|
import com.osglab.account.features.integrity.AppAttestCrypto
|
|
import com.osglab.account.features.integrity.AppAttestRepository
|
|
import com.osglab.account.features.integrity.AppAttestService
|
|
import com.osglab.account.features.integrity.AppAttestVerifier
|
|
import com.osglab.account.features.integrity.AppleDeviceCheckClient
|
|
import com.osglab.account.features.integrity.BundledAppleAppAttestTrust
|
|
import com.osglab.account.features.integrity.DeviceCheckTrialClaimRepository
|
|
import com.osglab.account.features.integrity.DeviceCheckTrialService
|
|
import com.osglab.account.features.integrity.DeviceCheckVerifier
|
|
import com.osglab.account.features.integrity.ExposedAppAttestRepository
|
|
import com.osglab.account.features.integrity.ExposedDeviceCheckTrialClaimRepository
|
|
import com.osglab.account.features.integrity.IntegrityService
|
|
import com.osglab.account.features.integrity.LibraryAppAttestCrypto
|
|
import com.osglab.account.features.integrity.MysqlDeviceCheckTrialMutex
|
|
import com.osglab.account.features.integrity.RemoteDeviceCheckVerifier
|
|
import com.osglab.account.features.integrity.TrialCreditGranter
|
|
import com.osglab.account.features.integrity.UnavailableAppleDeviceCheckClient
|
|
import com.osglab.account.features.integrity.createDeviceCheckClient
|
|
import com.osglab.account.features.integrity.integrityRoutes
|
|
import com.osglab.account.features.inviteweb.InviteWebConfig
|
|
import com.osglab.account.features.inviteweb.ReferralLookupPort
|
|
import com.osglab.account.features.inviteweb.configureInviteWebRoutes
|
|
import com.osglab.account.features.referrals.routes.referralRoutes
|
|
import com.osglab.account.features.referrals.services.ReferralOperations
|
|
import com.osglab.account.features.referrals.services.ReferralService
|
|
import com.osglab.account.features.referrals.services.ReferralRiskIdentity
|
|
import com.osglab.account.features.referrals.services.ReferralRiskProvider
|
|
import com.osglab.account.features.referrals.services.UserRegistrationTimeProvider
|
|
import io.ktor.client.HttpClient
|
|
import io.ktor.client.engine.cio.CIO
|
|
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation as ClientContentNegotiation
|
|
import io.ktor.client.plugins.HttpTimeout
|
|
import io.ktor.client.plugins.websocket.WebSockets as ClientWebSockets
|
|
import io.ktor.http.ContentType
|
|
import io.ktor.http.HttpStatusCode
|
|
import io.ktor.serialization.kotlinx.json.json
|
|
import io.ktor.server.application.Application
|
|
import io.ktor.server.application.ApplicationStopped
|
|
import io.ktor.server.application.install
|
|
import io.ktor.server.plugins.callid.CallId
|
|
import io.ktor.server.plugins.callid.callIdMdc
|
|
import io.ktor.server.plugins.calllogging.CallLogging
|
|
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
|
import io.ktor.server.plugins.defaultheaders.DefaultHeaders
|
|
import io.ktor.server.plugins.forwardedheaders.XForwardedHeaders
|
|
import io.ktor.server.plugins.ratelimit.RateLimit
|
|
import io.ktor.server.plugins.ratelimit.RateLimitName
|
|
import io.ktor.server.plugins.ratelimit.rateLimit
|
|
import io.ktor.server.request.httpMethod
|
|
import io.ktor.server.response.respond
|
|
import io.ktor.server.response.respondText
|
|
import io.ktor.server.routing.get
|
|
import io.ktor.server.routing.Route
|
|
import io.ktor.server.routing.routing
|
|
import io.ktor.server.websocket.WebSockets
|
|
import kotlinx.serialization.json.Json
|
|
import kotlinx.coroutines.CancellationException
|
|
import kotlinx.coroutines.delay
|
|
import kotlinx.coroutines.isActive
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.runBlocking
|
|
import org.koin.core.module.Module
|
|
import org.koin.dsl.module
|
|
import org.koin.ktor.ext.getKoin
|
|
import org.koin.ktor.plugin.Koin
|
|
import org.koin.logger.slf4jLogger
|
|
import java.time.Duration
|
|
import java.time.Instant
|
|
import java.util.UUID
|
|
import javax.crypto.Mac
|
|
import javax.crypto.spec.SecretKeySpec
|
|
import kotlin.time.Duration.Companion.minutes
|
|
|
|
private val JSON = Json {
|
|
ignoreUnknownKeys = true
|
|
explicitNulls = false
|
|
encodeDefaults = true
|
|
}
|
|
|
|
fun Application.module() {
|
|
val appConfig = AppConfig.from(environment.config)
|
|
|
|
install(ContentNegotiation) {
|
|
json(JSON)
|
|
}
|
|
install(DefaultHeaders) {
|
|
header("X-Content-Type-Options", "nosniff")
|
|
header("X-Frame-Options", "DENY")
|
|
header("Referrer-Policy", "no-referrer")
|
|
if (appConfig.isProduction) {
|
|
header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
|
|
}
|
|
}
|
|
install(CallId) {
|
|
retrieveFromHeader("X-Request-ID")
|
|
verify { it.matches(REQUEST_ID) }
|
|
generate { UUID.randomUUID().toString() }
|
|
}
|
|
install(CallLogging) {
|
|
callIdMdc("requestId")
|
|
// Never include paths, headers or bodies: they may contain invitation or session tokens.
|
|
format { call -> "${call.request.httpMethod.value} status=${call.response.status()}" }
|
|
}
|
|
install(WebSockets) {
|
|
maxFrameSize = 4L * 1024 * 1024
|
|
masking = false
|
|
}
|
|
install(XForwardedHeaders)
|
|
install(RateLimit) {
|
|
register(AUTH_RATE_LIMIT) {
|
|
rateLimiter(limit = 10, refillPeriod = 1.minutes)
|
|
}
|
|
register(ACCOUNT_RATE_LIMIT) {
|
|
rateLimiter(limit = 60, refillPeriod = 1.minutes)
|
|
}
|
|
register(GATEWAY_RATE_LIMIT) {
|
|
rateLimiter(limit = 120, refillPeriod = 1.minutes)
|
|
}
|
|
register(PUBLIC_RATE_LIMIT) {
|
|
rateLimiter(limit = 120, refillPeriod = 1.minutes)
|
|
}
|
|
register(ADMIN_AUTH_RATE_LIMIT) {
|
|
rateLimiter(limit = 5, refillPeriod = 1.minutes)
|
|
}
|
|
register(ADMIN_API_RATE_LIMIT) {
|
|
rateLimiter(limit = 60, refillPeriod = 1.minutes)
|
|
}
|
|
}
|
|
installApiStatusPages()
|
|
|
|
install(Koin) {
|
|
slf4jLogger()
|
|
modules(accountServerModule(appConfig))
|
|
}
|
|
|
|
val koin = getKoin()
|
|
// Fail startup before accepting traffic if migrations or database connectivity fail.
|
|
koin.get<DatabaseFactory>().database
|
|
if (appConfig.admin.bootstrapEnabled) {
|
|
runBlocking {
|
|
koin.get<AdminBootstrapService>().initialize(
|
|
AdminBootstrapConfig(
|
|
enabled = true,
|
|
operatorId = appConfig.admin.bootstrapOperatorId,
|
|
username = appConfig.admin.bootstrapUsername,
|
|
passwordHash = appConfig.admin.bootstrapPasswordHash,
|
|
totpSecretBase32 = appConfig.admin.bootstrapTotpSecretBase32,
|
|
),
|
|
)
|
|
}
|
|
}
|
|
val sessionAuthenticator = koin.get<SessionAccessAuthenticator>()
|
|
installSessionAuthentication(sessionAuthenticator::authenticate)
|
|
val asrStreaming = if (appConfig.providers.volcengine.credentialsAvailable) {
|
|
val providerConfig = appConfig.providers.volcengine.toProviderConfig()
|
|
AsrStreamingService(
|
|
gateway = koin.get(),
|
|
upstream = KtorVolcengineAsrTransport(koin.get(), providerConfig),
|
|
scope = this,
|
|
)
|
|
} else {
|
|
null
|
|
}
|
|
|
|
launch {
|
|
while (isActive) {
|
|
try {
|
|
koin.get<AppleRevocationOutboxProcessor>().processPending()
|
|
} catch (exception: CancellationException) {
|
|
throw exception
|
|
} catch (_: Exception) {
|
|
// Durable outbox state is retried; never log sensitive token material.
|
|
}
|
|
try {
|
|
koin.get<GatewayReconciliationService>().reconcile()
|
|
} catch (exception: CancellationException) {
|
|
throw exception
|
|
} catch (_: Exception) {
|
|
// Durable settlement state is retried without logging provider data.
|
|
}
|
|
if (appConfig.admin.enabled) {
|
|
try {
|
|
koin.get<AdminSessionService>().cleanupInactive()
|
|
} catch (exception: CancellationException) {
|
|
throw exception
|
|
} catch (_: Exception) {
|
|
// Expired sessions are retried in bounded batches on the next cycle.
|
|
}
|
|
}
|
|
delay(60_000)
|
|
}
|
|
}
|
|
|
|
monitor.subscribe(ApplicationStopped) {
|
|
koin.get<HttpClient>().close()
|
|
koin.get<DatabaseFactory>().close()
|
|
}
|
|
|
|
routing {
|
|
healthRoutes(koin.get())
|
|
rateLimit(AUTH_RATE_LIMIT) {
|
|
authRoutes(koin.get())
|
|
}
|
|
rateLimit(ACCOUNT_RATE_LIMIT) {
|
|
accountRoutes(koin.get())
|
|
creditRoutes(koin.get(), koin.get())
|
|
referralRoutes(koin.get(), koin.get())
|
|
}
|
|
rateLimit(GATEWAY_RATE_LIMIT) {
|
|
configureGatewayRoutes(
|
|
service = koin.get(),
|
|
appIdentity = koin.get<GatewayIdentityPort>(),
|
|
gatewayIdentity = koin.get<GatewayAccessTokenPort>(),
|
|
grantService = koin.get(),
|
|
asrStreaming = asrStreaming,
|
|
)
|
|
}
|
|
rateLimit(PUBLIC_RATE_LIMIT) {
|
|
appleEventRoutes(koin.get())
|
|
configureInviteWebRoutes(koin.get(), koin.get())
|
|
integrityRoutes(koin.get())
|
|
}
|
|
if (appConfig.admin.enabled) {
|
|
adminWebRoutes()
|
|
rateLimit(ADMIN_API_RATE_LIMIT) {
|
|
adminApiRoutes(
|
|
config = appConfig,
|
|
authService = koin.get(),
|
|
sessionService = koin.get(),
|
|
statsService = koin.get(),
|
|
usersService = koin.get(),
|
|
grantService = koin.get(),
|
|
operatorService = koin.get(),
|
|
auditService = koin.get(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun Route.healthRoutes(databaseFactory: DatabaseFactory? = null) {
|
|
get("/health") { call.respondText("""{"status":"UP"}""", ContentType.Application.Json) }
|
|
get("/health/live") { call.respondText("""{"status":"UP"}""", ContentType.Application.Json) }
|
|
get("/health/ready") {
|
|
if (databaseFactory?.isReady() == true) {
|
|
call.respondText("""{"status":"UP"}""", ContentType.Application.Json)
|
|
} else {
|
|
call.respondText(
|
|
"""{"status":"DOWN"}""",
|
|
ContentType.Application.Json,
|
|
HttpStatusCode.ServiceUnavailable,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun accountServerModule(config: AppConfig): Module = module {
|
|
single { config }
|
|
single { DatabaseFactory(config.database) }
|
|
single { get<DatabaseFactory>().database }
|
|
single {
|
|
HttpClient(CIO) {
|
|
followRedirects = false
|
|
install(HttpTimeout) {
|
|
connectTimeoutMillis = 10_000
|
|
socketTimeoutMillis = 60_000
|
|
requestTimeoutMillis = 360_000
|
|
}
|
|
install(ClientContentNegotiation) {
|
|
json(JSON)
|
|
}
|
|
install(ClientWebSockets) {
|
|
maxFrameSize = 4L * 1024 * 1024
|
|
}
|
|
}
|
|
}
|
|
|
|
single { SessionJwt(config.session) }
|
|
single { FieldEncryptor(config.encryption.key) }
|
|
single { IdentityFingerprint(config.antiAbuse.identityHmacKey) }
|
|
single<AdminRepository> { ExposedAdminRepository(get()) }
|
|
single<AdminPasswordHasher> { BouncyCastleArgon2idPasswordHasher() }
|
|
single<AdminTotpVerifier> { HmacTotpVerifier() }
|
|
single {
|
|
val dummyPassword = "invalid-admin-password-constant-work".toCharArray()
|
|
try {
|
|
AdminAuthService(
|
|
repository = get(),
|
|
passwordHasher = get(),
|
|
dummyPasswordHash = get<AdminPasswordHasher>().hash(dummyPassword),
|
|
totpVerifier = get(),
|
|
fieldEncryptor = get(),
|
|
sessionTtl = Duration.ofHours(config.admin.sessionHours),
|
|
)
|
|
} finally {
|
|
dummyPassword.fill('\u0000')
|
|
}
|
|
}
|
|
single { AdminSessionService(get()) }
|
|
single { AdminBootstrapService(get(), get()) }
|
|
single { AdminOperatorService(get(), get(), get()) }
|
|
single { AdminAuditService(get()) }
|
|
single<AdminStatsRepository> { ExposedAdminStatsRepository(get()) }
|
|
single { AdminStatsService(get()) }
|
|
single<AdminUsersRepository> { ExposedAdminUsersRepository(get()) }
|
|
single { AdminUsersService(get()) }
|
|
single { AdminGrantService(get()) }
|
|
single<AppleJwksProvider> {
|
|
RemoteAppleJwksProvider(get(), config.apple.jwksUrl)
|
|
}
|
|
single { AppleIdentityTokenVerifier(config.apple, get()) }
|
|
single<AppleTokenClient> { createAppleTokenClient(get(), config.apple) }
|
|
single<AppleDeviceCheckClient> {
|
|
createDeviceCheckClient(get(), config.apple, config.integrity.appleEnvironment)
|
|
?: UnavailableAppleDeviceCheckClient()
|
|
}
|
|
single<DeviceCheckVerifier> { RemoteDeviceCheckVerifier(get()) }
|
|
single<AppAttestRepository> { ExposedAppAttestRepository(get()) }
|
|
single<AppAttestCrypto> {
|
|
LibraryAppAttestCrypto(
|
|
config = config.integrity,
|
|
certificateValidator = BundledAppleAppAttestTrust.validator(),
|
|
)
|
|
}
|
|
single { AppAttestService(get(), get(), config.integrity) }
|
|
single<AppAttestVerifier> { get<AppAttestService>() }
|
|
single { IntegrityService(config.integrity, get(), get()) }
|
|
single<DeviceCheckTrialClaimRepository> { ExposedDeviceCheckTrialClaimRepository(get()) }
|
|
single { MysqlDeviceCheckTrialMutex(get()) }
|
|
|
|
single<AuthRepository> { ExposedAuthRepository(get()) }
|
|
single { SessionAccessAuthenticator(get(), get()) }
|
|
single<AccountRepository> { ExposedAccountRepository(get(), get()) }
|
|
single<AppleEventRepository> { ExposedAppleEventRepository(get(), get(), config.antiAbuse) }
|
|
single { AppleEventVerifier(config.apple, get()) }
|
|
single { AppleEventService(get(), get()) }
|
|
|
|
single<BillingTransactionRunner> {
|
|
ExposedBillingTransactionRunner(get())
|
|
}
|
|
single {
|
|
CreditService(
|
|
transactions = get(),
|
|
referralRewards = ReferralRewardConfig(
|
|
inviterCredits = config.credits.referralInviter,
|
|
inviteeCredits = config.credits.referralInvitee,
|
|
),
|
|
)
|
|
}
|
|
single<CreditOperations> { get<CreditService>() }
|
|
single<TrialCreditGranter> {
|
|
TrialCreditGranter { accountId ->
|
|
get<CreditService>().grantSignupTrial(
|
|
userId = accountId,
|
|
credits = config.credits.signupTrial,
|
|
idempotencyKey = "internal:signup-trial:$accountId",
|
|
)
|
|
}
|
|
}
|
|
single {
|
|
DeviceCheckTrialService(
|
|
repository = get(),
|
|
client = get(),
|
|
creditGranter = get(),
|
|
policy = config.integrity.deviceCheckPolicy,
|
|
mutex = get<MysqlDeviceCheckTrialMutex>(),
|
|
)
|
|
}
|
|
single { ExposedGatewayRepository(get()) }
|
|
single<GatewayGrantRepository> { get<ExposedGatewayRepository>() }
|
|
single<GatewayGrantPort> { get<ExposedGatewayRepository>() }
|
|
single<GatewayUsagePort> { get<ExposedGatewayRepository>() }
|
|
single<AccountProvisioner> {
|
|
AccountProvisioner { accountId, deviceCheckToken ->
|
|
val granted = get<DeviceCheckTrialService>().claimAndGrant(accountId, deviceCheckToken)
|
|
if (deviceCheckToken != null && !granted) {
|
|
get<AuthRepository>().restrictAccountForAntiAbuse(
|
|
accountId,
|
|
java.time.Instant.now(),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
single {
|
|
SessionService(
|
|
repository = get(),
|
|
appleIdentityVerifier = get(),
|
|
appleTokenClient = get(),
|
|
integrityService = get(),
|
|
sessionJwt = get(),
|
|
fieldEncryptor = get(),
|
|
identityFingerprint = get(),
|
|
sessionConfig = config.session,
|
|
accountProvisioner = get(),
|
|
)
|
|
}
|
|
single { AppleRevocationOutboxProcessor(get(), get(), get()) }
|
|
single<AccountReauthenticator> {
|
|
AppleAccountReauthenticator(
|
|
identityVerifier = get(),
|
|
appleTokenClient = get(),
|
|
identityFingerprint = get(),
|
|
)
|
|
}
|
|
single {
|
|
AccountService(
|
|
repository = get(),
|
|
fieldEncryptor = get(),
|
|
antiAbuseConfig = config.antiAbuse,
|
|
revocationProcessor = get(),
|
|
reauthenticator = get(),
|
|
)
|
|
}
|
|
|
|
single<UserRegistrationTimeProvider> {
|
|
UserRegistrationTimeProvider { accountId ->
|
|
get<AccountRepository>().findById(accountId)?.createdAt
|
|
}
|
|
}
|
|
single<ReferralRiskProvider> {
|
|
ReferralRiskProvider { accountId ->
|
|
get<AccountRepository>().findById(accountId)?.let {
|
|
ReferralRiskIdentity(it.identityFingerprint, it.antiAbuseRestricted)
|
|
}
|
|
}
|
|
}
|
|
single {
|
|
ReferralService(
|
|
transactions = get(),
|
|
registrationTimeProvider = get(),
|
|
riskProvider = get(),
|
|
bindingWindow = Duration.ofDays(config.credits.referralBindingDays),
|
|
)
|
|
}
|
|
single<ReferralOperations> { get<ReferralService>() }
|
|
single<ReferralLookupPort> {
|
|
val transactions = get<BillingTransactionRunner>()
|
|
ReferralLookupPort { code ->
|
|
transactions.inTransaction { unit ->
|
|
val referralCode = unit.referrals.findCode(code)
|
|
referralCode?.campaignId
|
|
?.let(unit.referrals::findCampaign)
|
|
?.isActive(Instant.now()) == true
|
|
}
|
|
}
|
|
}
|
|
|
|
single { SessionIdentityAdapter(get()) }
|
|
single<AuthenticatedUserExtractor> { get<SessionIdentityAdapter>() }
|
|
single<GatewayIdentityPort> { get<SessionIdentityAdapter>() }
|
|
single {
|
|
GatewaySettings(
|
|
issuer = config.session.issuer,
|
|
audience = "${config.session.audience}-gateway",
|
|
accessTokenHmacSecret = deriveGatewaySecret(config.session.hmacSecret, "gateway-access"),
|
|
refreshTokenHmacSecret = deriveGatewaySecret(config.session.hmacSecret, "gateway-refresh"),
|
|
accessTokenLifetime = Duration.ofMinutes(5),
|
|
refreshTokenLifetime = Duration.ofDays(config.session.refreshDays),
|
|
maximumGrantLifetime = Duration.ofDays(config.session.gatewayGrantDays),
|
|
)
|
|
}
|
|
single { GatewayGrantService(get(), get()) }
|
|
single<GatewayAccessTokenPort> { GatewayBearerIdentity(get()) }
|
|
single<CreditReservationPort> {
|
|
CreditReservationAdapter(
|
|
creditService = get(),
|
|
llmModel = config.providers.deepSeek.model,
|
|
asrModel = config.providers.volcengine.resourceId,
|
|
)
|
|
}
|
|
single {
|
|
ProviderCatalog(configuredProviders(config, get()))
|
|
}
|
|
single { GatewayService(get(), get(), get(), get()) }
|
|
single { GatewayReconciliationService(get(), get()) }
|
|
single {
|
|
InviteWebConfig(
|
|
appStoreUrl = config.appStoreUrl,
|
|
appleAppId = "${config.integrity.appAttestTeamId}.${config.integrity.appAttestBundleId}",
|
|
universalLinkBaseUrl = config.inviteBaseUrl,
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun configuredProviders(config: AppConfig, client: HttpClient): List<GatewayProvider> =
|
|
buildList {
|
|
config.providers.deepSeek.apiKey?.let { apiKey ->
|
|
add(
|
|
DeepSeekProvider(
|
|
client,
|
|
DeepSeekConfig(
|
|
endpoint = config.providers.deepSeek.endpoint,
|
|
apiKey = apiKey,
|
|
model = config.providers.deepSeek.model,
|
|
),
|
|
),
|
|
)
|
|
}
|
|
if (config.providers.volcengine.credentialsAvailable) {
|
|
val providerConfig = config.providers.volcengine.toProviderConfig()
|
|
add(VolcengineAsrProvider(KtorVolcengineAsrTransport(client, providerConfig)))
|
|
}
|
|
}
|
|
|
|
private fun com.osglab.account.config.VolcengineConfig.toProviderConfig() =
|
|
VolcengineAsrConfig(
|
|
endpoint = endpoint,
|
|
resourceId = resourceId,
|
|
appId = appId,
|
|
accessToken = accessToken,
|
|
apiKey = apiKey,
|
|
)
|
|
|
|
private fun deriveGatewaySecret(master: ByteArray, context: String): ByteArray =
|
|
Mac.getInstance("HmacSHA256").run {
|
|
init(SecretKeySpec(master, "HmacSHA256"))
|
|
doFinal("osg-account-server:$context".toByteArray(Charsets.UTF_8))
|
|
}
|
|
|
|
private val REQUEST_ID = Regex("[A-Za-z0-9_-]{8,64}")
|
|
private val AUTH_RATE_LIMIT = RateLimitName("auth")
|
|
private val ACCOUNT_RATE_LIMIT = RateLimitName("account")
|
|
private val GATEWAY_RATE_LIMIT = RateLimitName("gateway")
|
|
private val PUBLIC_RATE_LIMIT = RateLimitName("public")
|
|
private val ADMIN_AUTH_RATE_LIMIT = RateLimitName("admin-auth")
|
|
private val ADMIN_API_RATE_LIMIT = RateLimitName("admin-api")
|