package world.respect.server import androidx.room.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import io.ktor.http.Url import io.ktor.server.config.ApplicationConfig import kotlinx.coroutines.runBlocking import kotlinx.io.files.Path import kotlinx.serialization.json.Json import org.koin.core.scope.Scope import org.koin.dsl.module import world.respect.datalayer.RespectAppDataSource import world.respect.datalayer.RespectAppDataSourceLocal import world.respect.datalayer.SchoolDataSource import world.respect.datalayer.SchoolDataSourceLocal import world.respect.datalayer.UidNumberMapper import world.respect.datalayer.db.RespectAppDataSourceDb import world.respect.datalayer.db.RespectAppDatabase import world.respect.datalayer.db.RespectSchoolDatabase import world.respect.datalayer.db.SchoolDataSourceDb import world.respect.datalayer.db.schooldirectory.SchoolDirectoryDataSourceDb import world.respect.datalayer.respect.model.SchoolDirectoryEntry import world.respect.datalayer.schooldirectory.SchoolDirectoryDataSourceLocal import world.respect.datalayer.shared.XXHashUidNumberMapper import world.respect.lib.primarykeygen.PrimaryKeyGenerator import world.respect.libutil.ext.sanitizedForFilename import world.respect.libxxhash.XXStringHasher import world.respect.libxxhash.jvmimpl.XXStringHasherCommonJvm import world.respect.server.account.invite.GetInviteInfoUseCaseServer import world.respect.server.domain.school.add.AddSchoolUseCase import world.respect.server.domain.school.add.AddServerManagedDirectoryCallback import world.respect.shared.domain.account.RespectAccount import world.respect.shared.domain.account.authwithpassword.GetTokenAndUserProfileWithUsernameAndPasswordDbImpl import world.respect.shared.domain.account.gettokenanduser.GetTokenAndUserProfileWithUsernameAndPasswordUseCase import world.respect.shared.domain.account.invite.GetInviteInfoUseCase import world.respect.shared.domain.account.invite.RedeemInviteUseCase import world.respect.shared.domain.account.invite.RedeemInviteUseCaseDb import world.respect.shared.domain.account.setpassword.SetPasswordUseCase import world.respect.shared.domain.account.setpassword.SetPasswordUseDbImpl import world.respect.shared.domain.account.validateauth.ValidateAuthorizationUseCase import world.respect.shared.domain.account.validateauth.ValidateAuthorizationUseCaseDbImpl import world.respect.shared.domain.school.RespectSchoolPath import world.respect.shared.domain.school.SchoolPrimaryKeyGenerator import world.respect.shared.util.di.RespectAccountScopeId import world.respect.shared.util.di.SchoolDirectoryEntryScopeId import java.io.File const val APP_DB_FILENAME = "respect-app.db" fun serverKoinModule( config: ApplicationConfig, dataDir: File = config.absoluteDataDir() ) = module { single { val dbFile = File(dataDir, APP_DB_FILENAME) Room.databaseBuilder(dbFile.absolutePath) .setDriver(BundledSQLiteDriver()) .addCallback(AddServerManagedDirectoryCallback(xxStringHasher = get())) .build() } single { Json { ignoreUnknownKeys = true } } single { XXStringHasherCommonJvm() } single { XXHashUidNumberMapper(xxStringHasher = get()) } single { PrimaryKeyGenerator(RespectAppDatabase.TABLE_IDS) } single { SchoolDirectoryDataSourceDb( respectAppDb = get(), json = get(), xxStringHasher = get() ) } single { RespectAppDataSourceDb( respectAppDatabase = get(), json = get(), xxStringHasher = get(), primaryKeyGenerator = get(), ) } single { get() } single { GetInviteInfoUseCaseServer( respectAppDb = get(), respectAppDataSource = get(), ) } single { AddSchoolUseCase( directoryDataSource = get().schoolDirectoryDataSource, schoolDirectoryEntryDataSource = get().schoolDirectoryEntryDataSource, ) } single { GetInviteInfoUseCaseServer( respectAppDb = get(), respectAppDataSource = get(), ) } /* * School scope: used as the basis for virtual hosting. */ scope { fun Scope.schoolUrl(): Url = SchoolDirectoryEntryScopeId.parse(id).schoolUrl scoped { val schoolDirName = schoolUrl().sanitizedForFilename() val schoolDirFile = File(dataDir, schoolDirName).also { if(!it.exists()) it.mkdirs() } RespectSchoolPath( path = Path(schoolDirFile.absolutePath) ) } scoped { val schoolPath: RespectSchoolPath = get() val appDb: RespectAppDatabase = get() val xxHasher: XXStringHasher = get() val schoolConfig = runBlocking { appDb.getSchoolConfigEntityDao().findByUid(xxHasher.hash(schoolUrl().toString())) } ?: throw IllegalStateException("School config not found for $id") val schoolConfigFile = File(schoolPath.path.toString()) val dbFile = schoolConfigFile.resolve(schoolConfig.dbUrl) Room.databaseBuilder(dbFile.absolutePath) .setDriver(BundledSQLiteDriver()) .build() } scoped { SetPasswordUseDbImpl( schoolDb = get(), xxHash = get() ) } scoped { ValidateAuthorizationUseCaseDbImpl(schoolDb = get()) } scoped { GetTokenAndUserProfileWithUsernameAndPasswordDbImpl( schoolDb = get(), xxHash = get(), ) } scoped { SchoolPrimaryKeyGenerator( PrimaryKeyGenerator(SchoolPrimaryKeyGenerator.TABLE_IDS) ) } scoped { val schoolScopeId = SchoolDirectoryEntryScopeId.parse(id) RedeemInviteUseCaseDb( schoolDb = get(), schoolUrl = schoolScopeId.schoolUrl, schoolPrimaryKeyGenerator = get(), setPasswordUseCase = get(), getTokenAndUserProfileUseCase = get(), schoolDataSource = { schoolUrl, user -> getKoin().getOrCreateScope( RespectAccountScopeId(schoolUrl, user).scopeId ).get() }, uidNumberMapper = get(), ) } } /* * AccountScope: as per the client, the Account Scope is linked to a parent School scope. * * All server-side dependencies in the account scope are cheap wrappers e.g. the * SchoolDataSource wrapper (which is tied to a specific account guid) is kept in the AccountScope, * but the RespectSchoolDatabase which has the actual DB connection is kept in the school scope. * * Dependencies in the account scope use factory so they are not retained in memory */ scope { factory { val accountScopeId = RespectAccountScopeId.parse(id) val directoryEntryScopeId = SchoolDirectoryEntryScopeId( accountScopeId.schoolUrl, null ) linkTo( getKoin().getOrCreateScope( directoryEntryScopeId.scopeId ) ) SchoolDataSourceDb( schoolDb = get(), uidNumberMapper = get(), authenticatedUser = accountScopeId.accountPrincipalId ) } factory { get() } } }