Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1762: Add agency information to region #1902

Merged
merged 8 commits into from
Feb 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
query getFreinetAgencyByRegionId($regionId: Int!, $project: String!) {
agency: getFreinetAgencyByRegionId(regionId: $regionId, project: $project) {
agencyId
agencyName
apiAccessKey
dataTransferActivated
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ object Authorizer {
return user.projectId.value == projectId && user.role == Role.PROJECT_STORE_MANAGER.db_value
}

fun mayViewFreinetAgencyInformationInRegion(user: AdministratorEntity, regionId: Int): Boolean {
return user.role == Role.REGION_ADMIN.db_value && ProjectEntity.find { Projects.project eq EAK_BAYERN_PROJECT }
.single().id.value == user.projectId.value && user.regionId?.value == regionId
}

fun mayAddApiTokensInProject(user: AdministratorEntity): Boolean {
return transaction {
(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app.ehrenamtskarte.backend.common.database
import app.ehrenamtskarte.backend.auth.database.repos.AdministratorsRepository
import app.ehrenamtskarte.backend.auth.webservice.schema.types.Role
import app.ehrenamtskarte.backend.config.BackendConfiguration
import app.ehrenamtskarte.backend.freinet.util.FreinetAgenciesLoader
import app.ehrenamtskarte.backend.migration.assertDatabaseIsInSync
import app.ehrenamtskarte.backend.projects.database.insertOrUpdateProjects
import app.ehrenamtskarte.backend.regions.database.insertOrUpdateRegions
Expand Down Expand Up @@ -61,10 +62,11 @@ class Database {
}

fun setupInitialData(config: BackendConfiguration) {
val agencies = FreinetAgenciesLoader().loadAgenciesFromXml(config.projects)
transaction {
assertDatabaseIsInSync()
insertOrUpdateProjects(config)
insertOrUpdateRegions()
insertOrUpdateRegions(agencies)
insertOrUpdateCategories(Companion::executeScript)
createOrReplaceStoreFunctions(Companion::executeScript)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import app.ehrenamtskarte.backend.exception.service.NotFoundException
import app.ehrenamtskarte.backend.exception.service.NotImplementedException
import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import app.ehrenamtskarte.backend.exception.webservice.ExceptionSchemaConfig
import app.ehrenamtskarte.backend.freinet.webservice.freinetGraphQlParams
import app.ehrenamtskarte.backend.regions.utils.PostalCodesLoader
import app.ehrenamtskarte.backend.regions.webservice.regionsGraphQlParams
import app.ehrenamtskarte.backend.stores.webservice.storesGraphQlParams
Expand Down Expand Up @@ -37,7 +38,7 @@ class GraphQLHandler(
private val backendConfiguration: BackendConfiguration,
private val graphQLParams: GraphQLParams =
storesGraphQlParams stitch cardsGraphQlParams
stitch applicationGraphQlParams stitch regionsGraphQlParams stitch authGraphQlParams,
stitch applicationGraphQlParams stitch regionsGraphQlParams stitch authGraphQlParams stitch freinetGraphQlParams,
private val regionIdentifierByPostalCode: List<Pair<String, String>> = PostalCodesLoader.loadRegionIdentifierByPostalCodeMap()
) {
val config: SchemaGeneratorConfig = graphQLParams.config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class GeocodingConfig(val enabled: Boolean, val host: String)
data class CsvWriterConfig(val enabled: Boolean)
data class SmtpConfig(val host: String, val port: Int, val username: String, val password: String)
data class MatomoConfig(val siteId: Int, val accessToken: String)
data class FreinetConfig(val host: String, val path: String, val portalId: String, val accessToken: String)
data class ProjectConfig(
val id: String,
val importUrl: String,
Expand All @@ -35,7 +36,8 @@ data class ProjectConfig(
val timezone: ZoneId,
val selfServiceEnabled: Boolean,
val smtp: SmtpConfig,
val matomo: MatomoConfig?
val matomo: MatomoConfig?,
val freinet: FreinetConfig?
)

data class ServerConfig(val dataDirectory: String, val host: String, val port: String)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package app.ehrenamtskarte.backend.freinet.database

import app.ehrenamtskarte.backend.regions.database.Regions
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable

object FreinetAgencies : IntIdTable() {
val regionId = reference("regionId", Regions)
val agencyId = integer("agencyId").uniqueIndex()
val agencyName = varchar("agencyName", 255)
val apiAccessKey = varchar("apiAccessKey", 100)
val dataTransferActivated = bool("dataTransferActivated").default(false)
}

class FreinetAgenciesEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<FreinetAgenciesEntity>(FreinetAgencies)

var regionId by FreinetAgencies.regionId
var agencyId by FreinetAgencies.agencyId
var agencyName by FreinetAgencies.agencyName
var apiAccessKey by FreinetAgencies.apiAccessKey
var dataTransferActivated by FreinetAgencies.dataTransferActivated
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package app.ehrenamtskarte.backend.freinet.database

import app.ehrenamtskarte.backend.freinet.webservice.schema.types.FreinetApiAgency
import app.ehrenamtskarte.backend.regions.database.RegionEntity
import org.jetbrains.exposed.sql.SizedIterable

fun insertOrUpdateFreinetRegionInformation(
agency: FreinetApiAgency,
dbFreinetRegionInformation: SizedIterable<FreinetAgenciesEntity>,
regionEntity: RegionEntity
) {
val dbAgency = dbFreinetRegionInformation.find { it.agencyId == agency.agencyId }
if (dbAgency == null) {
FreinetAgenciesEntity.new {
regionId = regionEntity.id
agencyId = agency.agencyId
agencyName = agency.agencyName
apiAccessKey = agency.apiAccessKey
}
} else {
dbAgency.agencyId = agency.agencyId
dbAgency.agencyName = agency.agencyName
dbAgency.apiAccessKey = agency.apiAccessKey
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.ehrenamtskarte.backend.freinet.database.repos

import app.ehrenamtskarte.backend.freinet.database.FreinetAgencies
import app.ehrenamtskarte.backend.freinet.database.FreinetAgenciesEntity

object FreinetAgencyRepository {
fun getFreinetAgencyByRegionId(regionId: Int): FreinetAgenciesEntity? {
return FreinetAgenciesEntity.find { FreinetAgencies.regionId eq regionId }.singleOrNull()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package app.ehrenamtskarte.backend.freinet.util

import app.ehrenamtskarte.backend.common.webservice.EAK_BAYERN_PROJECT
import app.ehrenamtskarte.backend.config.ProjectConfig
import app.ehrenamtskarte.backend.exception.service.NotFoundException
import app.ehrenamtskarte.backend.freinet.webservice.schema.types.FreinetApiAgency
import app.ehrenamtskarte.backend.freinet.webservice.schema.types.XMLAgencies
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import io.ktor.client.HttpClient
import io.ktor.client.plugins.HttpRequestRetry
import io.ktor.client.plugins.HttpResponseValidator
import io.ktor.client.request.request
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.URLProtocol
import io.ktor.http.path
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory

class FreinetAgenciesLoader {
private val logger = LoggerFactory.getLogger(FreinetAgenciesLoader::class.java)
private val httpClient = HttpClient {
install(HttpRequestRetry) {
retryOnServerErrors(maxRetries = 3)
retryOnException(maxRetries = 3, retryOnTimeout = false)
exponentialDelay()
}
HttpResponseValidator {
validateResponse { response ->
if (response.status != HttpStatusCode.OK) {
throw Exception(response.status.toString())
}
}
}
}

fun loadAgenciesFromXml(projectConfigs: List<ProjectConfig>): List<FreinetApiAgency> {
val bayernConfig =
projectConfigs.find { it.id == EAK_BAYERN_PROJECT } ?: throw NotFoundException("Project config not found")
if (bayernConfig.freinet == null) {
logger.error("Couldn't find required freinet api parameters in backend config.")
return emptyList()
}
val freinetConfig = bayernConfig.freinet
try {
val response = runBlocking {
httpClient.request {
url {
protocol = URLProtocol.HTTP
host = freinetConfig.host
path(freinetConfig.path)
parameters.append("accessKey", freinetConfig.accessToken)
parameters.append("portalId", freinetConfig.portalId)
parameters.append("limit", "1000")
}
method = HttpMethod.Get
}.bodyAsText()
}

val xmlMapper = XmlMapper()
xmlMapper.registerModule(KotlinModule.Builder().build())
xmlMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT)
xmlMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)

return transformAndFilterAgencyData(xmlMapper.readValue(response, XMLAgencies::class.java))
} catch (e: Exception) {
logger.error("Couldn't fetch agency information: ", e)
return emptyList()
}
}

private fun transformAndFilterAgencyData(agencies: XMLAgencies): List<FreinetApiAgency> {
return agencies.agencies.mapNotNull {
FreinetApiAgency(
agencyId = it.agencyId!!.toInt(),
agencyName = it.agencyName!!,
apiAccessKey = it.accessKey!!,
officialRegionalKeys = it.officialRegionalKeys?.split(",") ?: emptyList()
)
}.distinctBy { it.agencyId }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package app.ehrenamtskarte.backend.freinet.webservice

import app.ehrenamtskarte.backend.auth.database.AdministratorEntity
import app.ehrenamtskarte.backend.auth.service.Authorizer
import app.ehrenamtskarte.backend.common.webservice.EAK_BAYERN_PROJECT
import app.ehrenamtskarte.backend.common.webservice.GraphQLContext
import app.ehrenamtskarte.backend.exception.service.ForbiddenException
import app.ehrenamtskarte.backend.exception.service.NotEakProjectException
import app.ehrenamtskarte.backend.exception.service.ProjectNotFoundException
import app.ehrenamtskarte.backend.exception.service.UnauthorizedException
import app.ehrenamtskarte.backend.freinet.database.repos.FreinetAgencyRepository
import app.ehrenamtskarte.backend.freinet.webservice.schema.types.FreinetAgency
import app.ehrenamtskarte.backend.projects.database.ProjectEntity
import app.ehrenamtskarte.backend.projects.database.Projects
import com.expediagroup.graphql.generator.annotations.GraphQLDescription
import graphql.schema.DataFetchingEnvironment
import org.jetbrains.exposed.sql.transactions.transaction

@Suppress("unused")
class FreinetAgencyQueryService {
@GraphQLDescription("Returns freinet agency information for a particular region. Works only for the EAK project.")
fun getFreinetAgencyByRegionId(dfe: DataFetchingEnvironment, regionId: Int, project: String): FreinetAgency? = transaction {
val context = dfe.getContext<GraphQLContext>()
val jwtPayload = context.enforceSignedIn()
val admin =
AdministratorEntity.findById(jwtPayload.adminId)
?: throw UnauthorizedException()
if (!Authorizer.mayViewFreinetAgencyInformationInRegion(admin, regionId)) {
throw ForbiddenException()
}
val projectEntity = ProjectEntity.find { Projects.project eq project }.firstOrNull()
?: throw ProjectNotFoundException(project)

if (projectEntity.project != EAK_BAYERN_PROJECT) {
throw NotEakProjectException()
}
val agency = FreinetAgencyRepository.getFreinetAgencyByRegionId(regionId)
agency?.let { FreinetAgency(agencyId = it.agencyId, apiAccessKey = agency.apiAccessKey, agencyName = agency.agencyName, dataTransferActivated = agency.dataTransferActivated) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package app.ehrenamtskarte.backend.freinet.webservice

import app.ehrenamtskarte.backend.common.webservice.GraphQLParams
import app.ehrenamtskarte.backend.common.webservice.createRegistryFromNamedDataLoaders
import com.expediagroup.graphql.generator.SchemaGeneratorConfig
import com.expediagroup.graphql.generator.TopLevelObject

val freinetGraphQlParams = GraphQLParams(
config = SchemaGeneratorConfig(supportedPackages = listOf("app.ehrenamtskarte.backend.freinet.webservice.schema")),
dataLoaderRegistry = createRegistryFromNamedDataLoaders(),
queries = listOf(
TopLevelObject(FreinetAgencyQueryService())
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.ehrenamtskarte.backend.freinet.webservice.schema.types

data class FreinetAgency(
val agencyId: Int,
val agencyName: String,
val apiAccessKey: String,
val dataTransferActivated: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app.ehrenamtskarte.backend.freinet.webservice.schema.types

data class FreinetApiAgency(
val officialRegionalKeys: List<String>,
val agencyId: Int,
val agencyName: String,
val apiAccessKey: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package app.ehrenamtskarte.backend.freinet.webservice.schema.types

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement

@JacksonXmlRootElement(localName = "agencies")
data class XMLAgencies(
@JsonProperty("mandant")
@JacksonXmlElementWrapper(useWrapping = false)
var agencies: ArrayList<XMLAgency> = ArrayList()
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package app.ehrenamtskarte.backend.freinet.webservice.schema.types

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData

data class XMLAgency(
@JsonProperty("agenturid")
@JacksonXmlCData
var agencyId: String?,
@JsonProperty("accessKey")
@JacksonXmlCData
var accessKey: String?,
@JsonProperty("ars")
@JacksonXmlCData
var officialRegionalKeys: String?,
@JsonProperty("agenturname")
@JacksonXmlCData
var agencyName: String?
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import app.ehrenamtskarte.backend.application.database.Applications
import app.ehrenamtskarte.backend.auth.database.Administrators
import app.ehrenamtskarte.backend.auth.database.ApiTokens
import app.ehrenamtskarte.backend.cards.database.Cards
import app.ehrenamtskarte.backend.freinet.database.FreinetAgencies
import app.ehrenamtskarte.backend.migration.database.Migrations
import app.ehrenamtskarte.backend.projects.database.Projects
import app.ehrenamtskarte.backend.regions.database.Regions
Expand All @@ -30,6 +31,7 @@ object TablesRegistry {
Contacts,
Categories,
UserEntitlements,
ApiTokens
ApiTokens,
FreinetAgencies
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ object MigrationsRegistry {
V0022_AddTypeToApiToken(),
V0023_DropTypeDefaultOfApiToken(),
V0024_AddNewRoleToRoleRegionCombinationConstraint(),
V0025_AddSourceFieldToApplicationVerifications()
V0025_AddSourceFieldToApplicationVerifications(),
V0026_AddFreinetAgenciesTable()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package app.ehrenamtskarte.backend.migration.migrations

import app.ehrenamtskarte.backend.migration.Migration
import app.ehrenamtskarte.backend.migration.Statement

/**
* Adds freinetagencies table
*/
@Suppress("ClassName")
internal class V0026_AddFreinetAgenciesTable : Migration() {
override val migrate: Statement = {
exec(
"""
CREATE TABLE freinetagencies (
id SERIAL PRIMARY KEY ,
"regionId" INTEGER NOT NULL,
"agencyId" INTEGER NOT NULL,
"agencyName" character varying(255) NOT NULL,
"apiAccessKey" character varying(100) NOT NULL,
"dataTransferActivated" BOOLEAN NOT NULL default false
);
ALTER TABLE freinetagencies ADD CONSTRAINT fk_freinetagencies_regionid__id FOREIGN KEY ("regionId") REFERENCES regions(id) ON DELETE RESTRICT ON UPDATE RESTRICT;
ALTER TABLE freinetagencies ADD CONSTRAINT freinetagencies_agencyid_unique UNIQUE ("agencyId");
""".trimIndent()
)
}
}
Loading