From 157fb2d2d4958a237318e9759998e2f50b483ede Mon Sep 17 00:00:00 2001 From: Moritz Hofmeister <62036655+hofi99@users.noreply.github.com> Date: Tue, 17 Aug 2021 10:45:58 +0200 Subject: [PATCH] Migrate to ktor client and kotlinx.serialization (#100) --- .idea/codeStyles/Project.xml | 1 + common/build.gradle.kts | 7 +- .../studo/campusqr/common/ClientPayloads.kt | 59 ++++--- moderatorFrontend/build.gradle.kts | 5 + .../AccessManagementDetails.kt | 34 ++-- .../AccessManagementRow.kt | 10 +- .../views/guestCheckIn/AddGuestCheckIn.kt | 3 +- .../guestCheckInOverview/GuestCheckInRow.kt | 6 +- .../kotlin/views/locations/AddLocation.kt | 6 +- .../locationsOverview/LocationTableRow.kt | 7 +- .../src/main/kotlin/views/login/MailLogin.kt | 5 +- .../src/main/kotlin/views/report/Report.kt | 18 +-- .../src/main/kotlin/views/users/AddUser.kt | 11 +- .../main/kotlin/views/users/UserTableRow.kt | 7 +- .../src/main/kotlin/webcore/NetworkManager.kt | 145 ++++++++---------- .../com/studo/campusqr/database/Payloads.kt | 6 +- .../campusqr/endpoints/AccessManagement.kt | 8 +- .../studo/campusqr/endpoints/ReportData.kt | 11 +- 18 files changed, 164 insertions(+), 185 deletions(-) diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 6a3b19e..b05025b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -8,6 +8,7 @@ </value> </option> <option name="RIGHT_MARGIN" value="140" /> + <option name="FORMATTER_TAGS_ENABLED" value="true" /> <JetCodeStyleSettings> <option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" /> </JetCodeStyleSettings> diff --git a/common/build.gradle.kts b/common/build.gradle.kts index ce3b658..8b692f5 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("multiplatform") + kotlin("plugin.serialization") version "1.5.20" } kotlin { @@ -10,7 +11,11 @@ kotlin { } sourceSets { - val commonMain by getting + val commonMain by getting { + dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") + } + } val commonTest by getting } } diff --git a/common/src/commonMain/kotlin/com/studo/campusqr/common/ClientPayloads.kt b/common/src/commonMain/kotlin/com/studo/campusqr/common/ClientPayloads.kt index 1953440..2534ea8 100644 --- a/common/src/commonMain/kotlin/com/studo/campusqr/common/ClientPayloads.kt +++ b/common/src/commonMain/kotlin/com/studo/campusqr/common/ClientPayloads.kt @@ -1,18 +1,19 @@ package com.studo.campusqr.common +import kotlinx.serialization.Serializable interface ClientPayload +@Serializable class ClientLocation( val id: String, val name: String, var checkInCount: Int?, - val accessType: String, + val accessType: LocationAccessType, val seatCount: Int?, ) : ClientPayload -val ClientLocation.accessTypeEnum get() = LocationAccessType.valueOf(accessType) - +@Serializable class UserData( var appName: String, var clientUser: ClientUser? = null, // Null when unauthenticated @@ -22,17 +23,19 @@ class UserData( val UserData.isAuthenticated get() = clientUser != null +@Serializable class ReportData( val impactedUsersCount: Int, - val impactedUsersEmails: Array<String>, + val impactedUsersEmails: List<String>, val impactedUsersEmailsCsvData: String, - val reportedUserLocations: Array<UserLocation>, + val reportedUserLocations: List<UserLocation>, val reportedUserLocationsCsv: String, val reportedUserLocationsCsvFileName: String, val startDate: String, val endDate: String, val impactedUsersEmailsCsvFileName: String, ) : ClientPayload { + @Serializable class UserLocation( val locationId: String, val locationName: String, @@ -41,25 +44,25 @@ class ReportData( val date: String, val seat: Int?, val potentialContacts: Int, - val filteredSeats: Array<Int>?, + val filteredSeats: List<Int>?, ) } +@Serializable class LocationVisitData( val csvData: String, val csvFileName: String, ) : ClientPayload +@Serializable class ClientUser( val id: String, val email: String, val name: String, - val permissionsRaw: Array<String>, + val permissions: Set<UserPermission>, val firstLoginDate: String, ) : ClientPayload -val ClientUser.permissions: Set<UserPermission> get() = permissionsRaw.map { UserPermission.valueOf(it) }.toSet() - // Keep in sync with BackendUser val ClientUser.canEditUsers get() = UserPermission.EDIT_USERS in permissions val ClientUser.canEditLocations get() = UserPermission.EDIT_LOCATIONS in permissions @@ -68,85 +71,98 @@ val ClientUser.canEditAnyLocationAccess get() = canEditOwnLocationAccess || canE val ClientUser.canEditOwnLocationAccess get() = UserPermission.EDIT_OWN_ACCESS in permissions val ClientUser.canEditAllLocationAccess get() = UserPermission.EDIT_ALL_ACCESS in permissions +@Serializable class AccessManagementData( - val accessManagement: Array<ClientAccessManagement>, + val accessManagement: List<ClientAccessManagement>, val clientLocation: ClientLocation?, ) : ClientPayload +@Serializable class AccessManagementExportData( - val permits: Array<Permit>, + val permits: List<Permit>, val clientLocation: ClientLocation?, ) : ClientPayload { + @Serializable class Permit( val dateRange: ClientDateRange, val email: String, ) } +@Serializable class ClientAccessManagement( val id: String, val locationName: String, val locationId: String, - val allowedEmails: Array<String>, - val dateRanges: Array<ClientDateRange>, + val allowedEmails: List<String>, + val dateRanges: List<ClientDateRange>, val note: String, val reason: String, ) : ClientPayload +@Serializable class ClientDateRange( val from: Double, val to: Double, ) : ClientPayload +@Serializable class NewAccess( val locationId: String, - val allowedEmails: Array<String>, - val dateRanges: Array<ClientDateRange>, + val allowedEmails: List<String>, + val dateRanges: List<ClientDateRange>, val note: String, val reason: String, ) : ClientPayload +@Serializable class NewUserData( val email: String, val name: String, val password: String, - val permissions: Array<String>, + val permissions: List<String>, ) : ClientPayload +@Serializable class EditUserData( val userId: String? = null, val name: String?, val password: String?, - val permissions: Array<String>?, + val permissions: List<String>?, ) : ClientPayload +@Serializable class EditAccess( val locationId: String? = null, - val allowedEmails: Array<String>? = null, - val dateRanges: Array<ClientDateRange>? = null, + val allowedEmails: List<String>? = null, + val dateRanges: List<ClientDateRange>? = null, val note: String? = null, val reason: String? = null, ) : ClientPayload +@Serializable class CreateLocation( val name: String, val accessType: LocationAccessType, val seatCount: Int?, ) : ClientPayload +@Serializable class EditLocation( val name: String, val accessType: LocationAccessType, val seatCount: Int?, ) : ClientPayload +@Serializable class ClientSeatFilter( val id: String, val locationId: String, val seat: Int, - val filteredSeats: Array<Int>, + val filteredSeats: List<Int>, ) : ClientPayload +@Serializable class ActiveCheckIn( val id: String, val locationId: String, @@ -156,15 +172,18 @@ class ActiveCheckIn( val email: String, ) : ClientPayload +@Serializable class EditSeatFilter( val seat: Int, val filteredSeats: List<Int>, ) : ClientPayload +@Serializable class DeleteSeatFilter( val seat: Int, ) : ClientPayload +@Serializable class LiveCheckIn( val activeCheckIns: Int, val qrCodeContent: String? diff --git a/moderatorFrontend/build.gradle.kts b/moderatorFrontend/build.gradle.kts index a86e447..e728858 100644 --- a/moderatorFrontend/build.gradle.kts +++ b/moderatorFrontend/build.gradle.kts @@ -23,6 +23,11 @@ dependencies { api("org.jetbrains.kotlin-wrappers:kotlin-react-dom:17.0.2-pre.212-kotlin-1.5.10") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") + // kotlinx-serialization + Ktor client + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.2.1") + implementation("io.ktor:ktor-client-core:1.6.1") + implementation("io.ktor:ktor-client-serialization:1.6.1") + implementation(npm("normalize.css", "8.0.1")) implementation(devNpm("style-loader", "2.0.0")) implementation(devNpm("css-loader", "5.2.6")) diff --git a/moderatorFrontend/src/main/kotlin/views/accessManagement/AccessManagementDetails.kt b/moderatorFrontend/src/main/kotlin/views/accessManagement/AccessManagementDetails.kt index d8a47b9..2dff8df 100644 --- a/moderatorFrontend/src/main/kotlin/views/accessManagement/AccessManagementDetails.kt +++ b/moderatorFrontend/src/main/kotlin/views/accessManagement/AccessManagementDetails.kt @@ -120,17 +120,15 @@ class AddLocation(props: AccessManagementDetailsProps) : RComponent<AccessManage setState { showProgress = true } val response = NetworkManager.post<String>( url = "$apiBase/access/create", - json = JSON.stringify( - NewAccess( - locationId = state.selectedLocation!!.id, - // Add state.getPermittedEmailsFromTextField(), to make sure that any un-submitted emails get added - allowedEmails = state.permittedPeopleList.toTypedArray() + state.getPermittedEmailsFromTextField(), - dateRanges = state.timeSlots.toTypedArray(), - note = state.accessControlNoteTextFieldValue, - reason = state.accessControlReasonTextFieldValue - ) + body = NewAccess( + locationId = state.selectedLocation!!.id, + // Add state.getPermittedEmailsFromTextField(), to make sure that any un-submitted emails get added + allowedEmails = state.permittedPeopleList + state.getPermittedEmailsFromTextField(), + dateRanges = state.timeSlots, + note = state.accessControlNoteTextFieldValue, + reason = state.accessControlReasonTextFieldValue + ) ) - ) setState { showProgress = false } @@ -142,15 +140,13 @@ class AddLocation(props: AccessManagementDetailsProps) : RComponent<AccessManage val accessManagementId = (props.config as Config.Edit).accessManagement.id val response = NetworkManager.post<String>( url = "$apiBase/access/$accessManagementId/edit", - json = JSON.stringify( - EditAccess( - locationId = state.selectedLocation?.id, - // Add state.getPermittedEmailsFromTextField(), to make sure that any un-submitted emails get added - allowedEmails = state.permittedPeopleList.toTypedArray() + state.getPermittedEmailsFromTextField(), - dateRanges = state.timeSlots.toTypedArray(), - note = state.accessControlNoteTextFieldValue, - reason = state.accessControlReasonTextFieldValue - ) + body = EditAccess( + locationId = state.selectedLocation?.id, + // Add state.getPermittedEmailsFromTextField(), to make sure that any un-submitted emails get added + allowedEmails = state.permittedPeopleList + state.getPermittedEmailsFromTextField(), + dateRanges = state.timeSlots, + note = state.accessControlNoteTextFieldValue, + reason = state.accessControlReasonTextFieldValue ) ) setState { diff --git a/moderatorFrontend/src/main/kotlin/views/accessManagement/accessManagementOverview/AccessManagementRow.kt b/moderatorFrontend/src/main/kotlin/views/accessManagement/accessManagementOverview/AccessManagementRow.kt index 5f08c8b..76c72a1 100644 --- a/moderatorFrontend/src/main/kotlin/views/accessManagement/accessManagementOverview/AccessManagementRow.kt +++ b/moderatorFrontend/src/main/kotlin/views/accessManagement/accessManagementOverview/AccessManagementRow.kt @@ -150,20 +150,14 @@ class AccessManagementTableRow : RComponent<AccessManagementTableRowProps, Acces }), MenuItem(text = Strings.duplicate.get(), icon = fileCopyOutlinedIcon, onClick = { launch { - val response = NetworkManager.post<String>( - "$apiBase/access/${props.config.accessManagement.id}/duplicate", - params = null - ) + val response = NetworkManager.post<String>("$apiBase/access/${props.config.accessManagement.id}/duplicate") props.config.onOperationFinished(Operation.Duplicate, response == "ok") } }), MenuItem(text = Strings.delete.get(), icon = deleteIcon, onClick = { if (window.confirm(Strings.access_control_delete_are_your_sure.get())) { launch { - val response = NetworkManager.post<String>( - "$apiBase/access/${props.config.accessManagement.id}/delete", - params = null - ) + val response = NetworkManager.post<String>("$apiBase/access/${props.config.accessManagement.id}/delete") props.config.onOperationFinished(Operation.Delete, response == "ok") } } diff --git a/moderatorFrontend/src/main/kotlin/views/guestCheckIn/AddGuestCheckIn.kt b/moderatorFrontend/src/main/kotlin/views/guestCheckIn/AddGuestCheckIn.kt index 9f43148..94d2386 100644 --- a/moderatorFrontend/src/main/kotlin/views/guestCheckIn/AddGuestCheckIn.kt +++ b/moderatorFrontend/src/main/kotlin/views/guestCheckIn/AddGuestCheckIn.kt @@ -21,7 +21,6 @@ import webcore.materialUI.muiAutocomplete import webcore.materialUI.muiButton import webcore.materialUI.textField import webcore.materialUI.withStyles -import kotlin.js.json interface AddGuestCheckInProps : RProps { class Config( @@ -83,7 +82,7 @@ class AddGuestCheckIn : RComponent<AddGuestCheckInProps, AddGuestCheckInState>() val locationId = locationIdWithSeat(state.selectedLocation!!.id, state.seatInputValue) val response = NetworkManager.post<String>( url = "$baseUrl/location/$locationId/guestCheckIn", - params = json("email" to state.personEmailTextFieldValue) + body = mapOf("email" to state.personEmailTextFieldValue) ) setState { showProgress = false diff --git a/moderatorFrontend/src/main/kotlin/views/guestCheckIn/guestCheckInOverview/GuestCheckInRow.kt b/moderatorFrontend/src/main/kotlin/views/guestCheckIn/guestCheckInOverview/GuestCheckInRow.kt index 1c90a54..d047703 100644 --- a/moderatorFrontend/src/main/kotlin/views/guestCheckIn/guestCheckInOverview/GuestCheckInRow.kt +++ b/moderatorFrontend/src/main/kotlin/views/guestCheckIn/guestCheckInOverview/GuestCheckInRow.kt @@ -17,7 +17,6 @@ import webcore.materialUI.mTableCell import webcore.materialUI.mTableRow import webcore.materialUI.muiButton import webcore.materialUI.withStyles -import kotlin.js.json interface GuestCheckInRowProps : RProps { var classes: GuestCheckInRowClasses @@ -55,9 +54,8 @@ class GuestCheckInRow : RComponent<GuestCheckInRowProps, GuestCheckInRowState>() val locationId = with(props.config.activeCheckIn) { locationIdWithSeat(locationId, seat) } launch { val response = NetworkManager.post<String>( - "$apiBase/location/$locationId/checkout", params = json( - "email" to props.config.activeCheckIn.email - ) + "$apiBase/location/$locationId/checkout", + body = mapOf("email" to props.config.activeCheckIn.email) ) if (response == "ok") { props.config.onCheckedOut() diff --git a/moderatorFrontend/src/main/kotlin/views/locations/AddLocation.kt b/moderatorFrontend/src/main/kotlin/views/locations/AddLocation.kt index e150b23..3866feb 100644 --- a/moderatorFrontend/src/main/kotlin/views/locations/AddLocation.kt +++ b/moderatorFrontend/src/main/kotlin/views/locations/AddLocation.kt @@ -3,7 +3,6 @@ package views.locations import app.GlobalCss import com.studo.campusqr.common.ClientLocation import com.studo.campusqr.common.LocationAccessType -import com.studo.campusqr.common.accessTypeEnum import com.studo.campusqr.common.extensions.format import kotlinext.js.js import org.w3c.dom.events.Event @@ -19,7 +18,6 @@ import webcore.NetworkManager import webcore.extensions.inputValue import webcore.extensions.launch import webcore.materialUI.* -import kotlin.js.json interface AddLocationProps : RProps { sealed class Config(val onFinished: (response: String?) -> Unit) { @@ -45,7 +43,7 @@ class AddLocation(props: AddLocationProps) : RComponent<AddLocationProps, AddLoc locationCreationInProgress = false locationTextFieldError = "" locationTextFieldValue = (props.config as? Config.Edit)?.location?.name ?: "" - locationAccessType = (props.config as? Config.Edit)?.location?.accessTypeEnum ?: LocationAccessType.FREE + locationAccessType = (props.config as? Config.Edit)?.location?.accessType ?: LocationAccessType.FREE locationSeatCount = (props.config as? Config.Edit)?.location?.seatCount } @@ -57,7 +55,7 @@ class AddLocation(props: AddLocationProps) : RComponent<AddLocationProps, AddLoc } val response = NetworkManager.post<String>( url = url, - params = json( + body = mapOf( "name" to state.locationTextFieldValue, "accessType" to state.locationAccessType.name, "seatCount" to state.locationSeatCount diff --git a/moderatorFrontend/src/main/kotlin/views/locations/locationsOverview/LocationTableRow.kt b/moderatorFrontend/src/main/kotlin/views/locations/locationsOverview/LocationTableRow.kt index 9d69469..19b7b7c 100644 --- a/moderatorFrontend/src/main/kotlin/views/locations/locationsOverview/LocationTableRow.kt +++ b/moderatorFrontend/src/main/kotlin/views/locations/locationsOverview/LocationTableRow.kt @@ -77,7 +77,7 @@ class LocationTableRow : RComponent<LocationTableRowProps, LocationTableRowState } } mTableCell { - +props.config.location.accessTypeEnum.localizedString.get() + +props.config.location.accessType.localizedString.get() } mTableCell { +(props.config.location.seatCount?.toString() ?: Strings.undefined.get()) @@ -135,10 +135,7 @@ class LocationTableRow : RComponent<LocationTableRowProps, LocationTableRowState MenuItem(text = Strings.location_delete.get(), icon = deleteIcon, onClick = { if (window.confirm(Strings.location_delete_are_you_sure.get())) { launch { - val response = NetworkManager.post<String>( - "$apiBase/location/${props.config.location.id}/delete", - params = null - ) + val response = NetworkManager.post<String>("$apiBase/location/${props.config.location.id}/delete") props.config.onDeleteFinished(response) } } diff --git a/moderatorFrontend/src/main/kotlin/views/login/MailLogin.kt b/moderatorFrontend/src/main/kotlin/views/login/MailLogin.kt index 08fa4fd..d1a9115 100644 --- a/moderatorFrontend/src/main/kotlin/views/login/MailLogin.kt +++ b/moderatorFrontend/src/main/kotlin/views/login/MailLogin.kt @@ -21,7 +21,6 @@ import webcore.extensions.launch import webcore.materialUI.textField import webcore.materialUI.typography import webcore.materialUI.withStyles -import kotlin.js.json interface MailLoginProps : RProps { var classes: MailLoginClasses @@ -47,11 +46,11 @@ class MailLogin : LoginDetailComponent<MailLoginProps, MailLoginState>() { launch { val response: LoginResult? = NetworkManager.post<String>( url = "$apiBase/user/login", - params = json( + body = mapOf( "email" to state.email, "password" to state.password ), - headers = json( + headers = mapOf( "csrfToken" to document.querySelector("meta[name='csrfToken']")!!.getAttribute("content")!! ) )?.let { result -> LoginResult.values().find { it.name == result } ?: LoginResult.UNKNOWN_ERROR } diff --git a/moderatorFrontend/src/main/kotlin/views/report/Report.kt b/moderatorFrontend/src/main/kotlin/views/report/Report.kt index ad06db5..7fca229 100644 --- a/moderatorFrontend/src/main/kotlin/views/report/Report.kt +++ b/moderatorFrontend/src/main/kotlin/views/report/Report.kt @@ -24,7 +24,6 @@ import webcore.extensions.inputValue import webcore.extensions.launch import webcore.materialUI.* import kotlin.js.Date -import kotlin.js.json interface ReportProps : RProps { var classes: ReportClasses @@ -78,10 +77,8 @@ class Report : RComponent<ReportProps, ReportState>() { deleteFilter(userLocation) } else { val response = NetworkManager.post<String>( - "$apiBase/location/${userLocation.locationId}/editSeatFilter", params = json( - "seat" to userLocation.seat, - "filteredSeats" to filteredSeats - ) + "$apiBase/location/${userLocation.locationId}/editSeatFilter", + body = mapOf("seat" to userLocation.seat, "filteredSeats" to filteredSeats) ) setState { if (response == "ok") { @@ -98,9 +95,8 @@ class Report : RComponent<ReportProps, ReportState>() { private fun deleteFilter(userLocation: ReportData.UserLocation) = launch { setState { showProgress = true } val response = NetworkManager.post<String>( - "$apiBase/location/${userLocation.locationId}/deleteSeatFilter", params = json( - "seat" to userLocation.seat - ) + "$apiBase/location/${userLocation.locationId}/deleteSeatFilter", + body = mapOf("seat" to userLocation.seat) ) setState { if (response == "ok") { @@ -116,10 +112,8 @@ class Report : RComponent<ReportProps, ReportState>() { private fun traceContacts() = launch { setState { showProgress = true } val response = NetworkManager.post<ReportData>( - "$apiBase/report/list", params = json( - "email" to state.emailTextFieldValue, - "oldestDate" to state.infectionDate.getTime().toString() - ) + "$apiBase/report/list", + body = mapOf("email" to state.emailTextFieldValue, "oldestDate" to state.infectionDate.getTime().toString()) ) setState { showProgress = false diff --git a/moderatorFrontend/src/main/kotlin/views/users/AddUser.kt b/moderatorFrontend/src/main/kotlin/views/users/AddUser.kt index 8970acc..31d8fb8 100644 --- a/moderatorFrontend/src/main/kotlin/views/users/AddUser.kt +++ b/moderatorFrontend/src/main/kotlin/views/users/AddUser.kt @@ -67,14 +67,12 @@ class AddUser(props: AddUserProps) : RComponent<AddUserProps, AddUserState>(prop setState { userCreationInProgress = true } val response = NetworkManager.post<String>( url = "$apiBase/user/create", - json = JSON.stringify( - NewUserData( + body = NewUserData( email = state.userEmailTextFieldValue, password = state.userPasswordTextFieldValue, name = state.userNameTextFieldValue, - permissions = state.userPermissions.map { it.name }.toTypedArray() + permissions = state.userPermissions.map { it.name } ) - ) ) setState { userCreationInProgress = false @@ -86,17 +84,14 @@ class AddUser(props: AddUserProps) : RComponent<AddUserProps, AddUserState>(prop setState { userCreationInProgress = true } val response = NetworkManager.post<String>( url = "$apiBase/user/edit", - json = JSON.stringify( - EditUserData( + body = EditUserData( userId = (props.config as Config.Edit).user.id, name = state.userNameTextFieldValue.emptyToNull(), password = state.userPasswordTextFieldValue.emptyToNull(), permissions = state.userPermissions .takeIf { (props.config as Config.Edit).user.permissions != it } ?.map { it.name } - ?.toTypedArray() ) - ) ) setState { userCreationInProgress = false diff --git a/moderatorFrontend/src/main/kotlin/views/users/UserTableRow.kt b/moderatorFrontend/src/main/kotlin/views/users/UserTableRow.kt index 9aa08b2..23f728a 100644 --- a/moderatorFrontend/src/main/kotlin/views/users/UserTableRow.kt +++ b/moderatorFrontend/src/main/kotlin/views/users/UserTableRow.kt @@ -2,7 +2,6 @@ package views.users import com.studo.campusqr.common.ClientUser import com.studo.campusqr.common.UserData -import com.studo.campusqr.common.permissions import kotlinext.js.js import kotlinx.browser.window import react.* @@ -15,7 +14,6 @@ import util.localizedString import webcore.* import webcore.extensions.launch import webcore.materialUI.* -import kotlin.js.json interface UserTableRowProps : RProps { class Config( @@ -113,9 +111,8 @@ class UserTableRow : RComponent<UserTableRowProps, UserTableRowState>() { if (window.confirm(Strings.user_delete_are_you_sure.get())) { launch { val response = NetworkManager.post<String>( - "$apiBase/user/delete", params = json( - "userId" to props.config.user.id - ) + "$apiBase/user/delete", + body = mapOf("userId" to props.config.user.id) ) props.config.onEditFinished(response) } diff --git a/moderatorFrontend/src/main/kotlin/webcore/NetworkManager.kt b/moderatorFrontend/src/main/kotlin/webcore/NetworkManager.kt index 2e04b25..4355ee5 100644 --- a/moderatorFrontend/src/main/kotlin/webcore/NetworkManager.kt +++ b/moderatorFrontend/src/main/kotlin/webcore/NetworkManager.kt @@ -1,62 +1,84 @@ package webcore -import kotlinext.js.getOwnPropertyNames +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.features.json.* +import io.ktor.client.features.json.serializer.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.util.* import kotlinx.browser.window -import org.w3c.dom.url.URLSearchParams -import org.w3c.fetch.RequestInit -import org.w3c.fetch.Response -import webcore.extensions.await -import webcore.extensions.awaitOrNull -import kotlin.js.Json -import kotlin.js.Promise -import kotlin.reflect.KClass +/** + * NetworkManager uses Ktor client with kotlinx.serialization to create network requests. + * All functions in this object check the "x-version-hash" header and reload the application if necessary. + */ object NetworkManager { - suspend inline fun <reified T : Any> get(url: String, urlParams: Json? = null): T? = get(url, urlParams, T::class) - - suspend fun <T : Any> get(url: String, urlParams: Json?, kClass: KClass<T>): T? { - val urlWithParams = url + (urlParams?.let { "?" + URLSearchParams(urlParams) } ?: "") - return window.fetch(urlWithParams).parseResponse(kClass) + val client = HttpClient { + install(JsonFeature) { + serializer = KotlinxSerializer() + } } - suspend inline fun <reified T : Any> post(url: String, params: Json? = null, headers: Json? = null): T? = - post(url, params, T::class, headers) - - suspend inline fun <reified T : Any> post(url: String, json: String? = null, headers: Json? = null): T? = - post(url, json, T::class, headers) - - suspend fun <T : Any> post(url: String, params: Json? = null, kClass: KClass<T>, headers: Json? = null): T? { - return post(url, JSON.stringify(params), kClass, headers) + suspend inline fun <reified T : Any> get( + url: String, + urlParams: Map<String, Any?> = emptyMap(), + headers: Map<String, Any?> = emptyMap(), + ): T? = try { + val response: HttpResponse = client.get(url) { + headers.forEach { header(it.key, it.value) } + urlParams.forEach { parameter(it.key, it.value) } + } + response.reloadIfLocalVersionIsOutdated() + response.receive<T>() + } catch (e: Exception) { + null } - suspend fun <T : Any> post(url: String, json: String? = null, kClass: KClass<T>, headers: Json? = null): T? { - val response = window.fetch(url, RequestInit().also { request -> - request.method = "POST" - json?.let { request.body = json } - headers?.let { request.headers = it } - }) - return response.parseResponse(kClass) + suspend inline fun <reified T : Any> post( + url: String, + urlParams: Map<String, Any?> = emptyMap(), + body: Any? = null, + headers: Map<String, Any?> = emptyMap(), + ): T? = try { + val response: HttpResponse = client.post(url) { + body?.let { + contentType(ContentType.Application.Json) + this.body = body + } + headers.forEach { header(it.key, it.value) } + urlParams.forEach { parameter(it.key, it.value) } + } + response.reloadIfLocalVersionIsOutdated() + response.receive<T>() + } catch (e: Exception) { + null } - suspend inline fun <reified T : Any> put(url: String, params: Json? = null): T? = put(url, params, T::class) - - suspend fun <T : Any> put(url: String, params: Json? = null, kClass: KClass<T>): T? { - val response = window.fetch(url, RequestInit().also { request -> - request.method = "PUT" - params?.let { request.body = JSON.stringify(it) } - }) - return response.parseResponse(kClass) + suspend inline fun <reified T : Any> put( + url: String, + urlParams: Map<String, Any?> = emptyMap(), + headers: Map<String, Any?> = emptyMap(), + ): T? = try { + val response: HttpResponse = client.put(url) { + headers.forEach { header(it.key, it.value) } + urlParams.forEach { parameter(it.key, it.value) } + } + response.reloadIfLocalVersionIsOutdated() + response.receive<T>() + } catch (e: Exception) { + null } // Holds a unique hash for the local frontend version // The server send a hash with every call, when this hash mismatches with the server version, the page is reloaded. private var localFrontendVersionHash: String? = null - private fun Response.reloadIfLocalVersionIsOutdated() { + fun HttpResponse.reloadIfLocalVersionIsOutdated() { // If server doesn't send the hash, this feature is not supported on the backend - val remoteVersionHash = headers.get("x-version-hash") ?: return - + val remoteVersionHash = headers.flattenEntries().toMap()["x-version-hash"] ?: return // Set the local version hash on the first call, the check for any changes on the subsequent calls when { localFrontendVersionHash == null -> localFrontendVersionHash = remoteVersionHash @@ -66,47 +88,4 @@ object NetworkManager { } } } - - private suspend inline fun <T : Any> Promise<Response>.parseResponse(kClass: KClass<T>): T? { - return try { - val response: Response = this.awaitOrNull() ?: return null - response.reloadIfLocalVersionIsOutdated() - @Suppress("UNCHECKED_CAST") - when (kClass) { - String::class -> response.toStringMessage() as T - Map::class -> response.toMapMessage() as T - else -> response.toJsonMessage<T>() - } - } catch (e: dynamic) { - console.log("Failed to parse response", this) - null - } - } - - private suspend fun <T> Response.toJsonMessage(): T? { - return if (status == 200.toShort()) { - this.json().await().asDynamic() as T - } else { - null - } - } - - private suspend fun Response.toMapMessage(): Map<String, Any?>? { - @Suppress("UNCHECKED_CAST_TO_EXTERNAL_INTERFACE") - val json = if (status == 200.toShort()) { - this.json().await().asDynamic() as Json - } else { - return null - } - return mutableMapOf<String, Any?>() - .apply { json.getOwnPropertyNames().forEach { key -> this[key] = json[key] } } - } - - private suspend fun Response.toStringMessage(): String? { - return if (status == 200.toShort()) { - text().await() - } else { - null - } - } } \ No newline at end of file diff --git a/server/src/main/kotlin/com/studo/campusqr/database/Payloads.kt b/server/src/main/kotlin/com/studo/campusqr/database/Payloads.kt index e731b23..2eca697 100644 --- a/server/src/main/kotlin/com/studo/campusqr/database/Payloads.kt +++ b/server/src/main/kotlin/com/studo/campusqr/database/Payloads.kt @@ -27,7 +27,7 @@ class BackendUser() : MongoMainEntry(), ClientPayloadable<ClientUser> { id = _id, email = email, name = name, - permissionsRaw = permissions.map { it.name }.toTypedArray(), + permissions = permissions, firstLoginDate = firstLoginDate?.toAustrianTime("dd.MM.yyyy") ?: LocalizedString( "Not logged in yet", @@ -64,7 +64,7 @@ class BackendLocation : MongoMainEntry(), ClientPayloadable<ClientLocation> { id = _id, name = name, checkInCount = checkInCount, - accessType = accessType.name, + accessType = accessType, seatCount = seatCount ) } @@ -92,7 +92,7 @@ class BackendSeatFilter : MongoMainEntry(), ClientPayloadable<ClientSeatFilter> id = _id, locationId = locationId, seat = seat, - filteredSeats = filteredSeats.toTypedArray() + filteredSeats = filteredSeats ) } diff --git a/server/src/main/kotlin/com/studo/campusqr/endpoints/AccessManagement.kt b/server/src/main/kotlin/com/studo/campusqr/endpoints/AccessManagement.kt index a84c3a1..ab92932 100644 --- a/server/src/main/kotlin/com/studo/campusqr/endpoints/AccessManagement.kt +++ b/server/src/main/kotlin/com/studo/campusqr/endpoints/AccessManagement.kt @@ -18,8 +18,8 @@ private fun BackendAccess.toClientClass(location: BackendLocation) = ClientAcces id = _id, locationName = location.name, locationId = location._id, - allowedEmails = allowedEmails.toTypedArray(), - dateRanges = dateRanges.map { it.toClientClass() }.toTypedArray(), + allowedEmails = allowedEmails, + dateRanges = dateRanges.map { it.toClientClass() }, note = note, reason = reason ) @@ -55,7 +55,7 @@ suspend fun AuthenticatedApplicationCall.listAccess() { respondObject( AccessManagementData( - accessManagement = accessManagement.toTypedArray(), + accessManagement = accessManagement, clientLocation = if (locationId != null) locations.values.firstOrNull()?.toClientClass(language) else null ) ) @@ -105,7 +105,7 @@ suspend fun AuthenticatedApplicationCall.listExportAccess() { respondObject( AccessManagementExportData( - permits = permits.toTypedArray(), + permits = permits, clientLocation = location?.toClientClass(language) ) ) diff --git a/server/src/main/kotlin/com/studo/campusqr/endpoints/ReportData.kt b/server/src/main/kotlin/com/studo/campusqr/endpoints/ReportData.kt index 5cfee94..9aed51c 100644 --- a/server/src/main/kotlin/com/studo/campusqr/endpoints/ReportData.kt +++ b/server/src/main/kotlin/com/studo/campusqr/endpoints/ReportData.kt @@ -191,7 +191,7 @@ internal suspend fun generateContactTracingReport(emails: List<String>, oldestDa ?.take(20) return ReportData( - impactedUsersEmails = impactedUsersEmails.toTypedArray(), + impactedUsersEmails = impactedUsersEmails, impactedUsersCount = impactedUsers.count(), reportedUserLocations = contacts.map { (reportedCheckIn, impactedCheckIns) -> val location = locationIdToLocationMap.getValue(reportedCheckIn.locationId) @@ -203,13 +203,16 @@ internal suspend fun generateContactTracingReport(emails: List<String>, oldestDa date = reportedCheckIn.date.toAustrianTime(yearAtBeginning = false), seat = reportedCheckIn.seat, potentialContacts = impactedCheckIns.count(), - filteredSeats = seatFilterMap[reportedCheckIn.filterKey()]?.filteredSeats?.toTypedArray(), + filteredSeats = seatFilterMap[reportedCheckIn.filterKey()]?.filteredSeats, ) - }.toTypedArray(), + }, impactedUsersEmailsCsvData = impactedUsersEmails.joinToString("\n"), impactedUsersEmailsCsvFileName = "${csvFilePrefix?.plus("-emails") ?: "emails"}.csv", reportedUserLocationsCsv = "sep=;\n" + reportedUserCheckIns.joinToString("\n") { - "${it.email};${it.date.toAustrianTime(yearAtBeginning = false)};\"${locationIdToLocationMap.getValue(it.locationId).name.replace("\"", "\"\"")}\";${it.seat ?: "-"}" + "${it.email};" + + "${it.date.toAustrianTime(yearAtBeginning = false)};" + + "\"${locationIdToLocationMap.getValue(it.locationId).name.replace("\"", "\"\"")}\";" + + (it.seat ?: "-") }, reportedUserLocationsCsvFileName = "${csvFilePrefix?.plus("-checkins") ?: "checkins"}.csv", startDate = oldestDate.toAustrianTime("dd.MM.yyyy"), -- GitLab