From cf17c3091747649c8b7399d68e12d6507e5d7c9d Mon Sep 17 00:00:00 2001 From: NovaFox161 Date: Fri, 17 May 2024 23:23:51 -0500 Subject: [PATCH] Cleanup calendar authentication implementation This pass brings this code more properly in-line with adopted patterns, updates the Google api dependencies, and begins defining the pattern for api wrappers --- .../cam/business/google/GoogleAuthService.kt | 52 ++++++++ .../cam/controllers/v1/SecurityController.kt | 8 +- .../cam/controllers/v1/TokenController.kt | 4 +- .../discal/cam/google/GoogleAuth.kt | 118 ------------------ .../discal/cam/json/google/ErrorData.kt | 6 - .../discal/cam/json/google/RefreshData.kt | 11 -- .../cam/managers/CalendarAuthManager.kt | 14 +-- .../discal/core/business/CalendarService.kt | 44 +++---- .../discal/core/business/api/CamApiWrapper.kt | 106 ++++++++++++++++ .../business/api/GoogleCalendarApiWrapper.kt | 78 ++++++++++++ .../discal/core/config/CacheConfig.kt | 4 +- ...alendarData.kt => CalendarMetadataData.kt} | 2 +- ...itory.kt => CalendarMetadataRepository.kt} | 8 +- .../object/network/discal/CredentialData.kt | 1 + .../new/{Calendar.kt => CalendarMetadata.kt} | 20 +-- .../core/object/new/model/ResponseModel.kt | 13 ++ .../discal/cam/SecurityValidateV1Request.kt} | 4 +- .../discal/cam/SecurityValidateV1Response.kt} | 5 +- .../new/model/discal/cam/TokenV1Model.kt | 8 ++ .../google/OauthV4RefreshTokenResponse.kt | 11 ++ .../discal/core/object/rest/ErrorResponse.kt | 2 +- .../discal/core/spring/SecurityWebFilter.kt | 45 +++---- .../core/wrapper/google/GoogleAuthWrapper.kt | 52 -------- .../org/dreamexposure/discal/typealiases.kt | 2 +- gradle.properties | 6 +- 25 files changed, 351 insertions(+), 273 deletions(-) create mode 100644 cam/src/main/kotlin/org/dreamexposure/discal/cam/business/google/GoogleAuthService.kt delete mode 100644 cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt delete mode 100644 cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt delete mode 100644 cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt create mode 100644 core/src/main/kotlin/org/dreamexposure/discal/core/business/api/CamApiWrapper.kt create mode 100644 core/src/main/kotlin/org/dreamexposure/discal/core/business/api/GoogleCalendarApiWrapper.kt rename core/src/main/kotlin/org/dreamexposure/discal/core/database/{CalendarData.kt => CalendarMetadataData.kt} (92%) rename core/src/main/kotlin/org/dreamexposure/discal/core/database/{CalendarRepository.kt => CalendarMetadataRepository.kt} (83%) rename core/src/main/kotlin/org/dreamexposure/discal/core/object/new/{Calendar.kt => CalendarMetadata.kt} (82%) create mode 100644 core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/ResponseModel.kt rename core/src/main/kotlin/org/dreamexposure/discal/core/object/{rest/v1/security/ValidateRequest.kt => new/model/discal/cam/SecurityValidateV1Request.kt} (67%) rename core/src/main/kotlin/org/dreamexposure/discal/core/object/{rest/v1/security/ValidateResponse.kt => new/model/discal/cam/SecurityValidateV1Response.kt} (53%) create mode 100644 core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/TokenV1Model.kt create mode 100644 core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/google/OauthV4RefreshTokenResponse.kt diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/google/GoogleAuthService.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/google/GoogleAuthService.kt new file mode 100644 index 000000000..8b0070c39 --- /dev/null +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/business/google/GoogleAuthService.kt @@ -0,0 +1,52 @@ +package org.dreamexposure.discal.cam.business.google + +import org.dreamexposure.discal.core.business.CalendarService +import org.dreamexposure.discal.core.business.CredentialService +import org.dreamexposure.discal.core.business.api.GoogleCalendarApiWrapper +import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException +import org.dreamexposure.discal.core.exceptions.NotFoundException +import org.dreamexposure.discal.core.extensions.isExpiredTtl +import org.dreamexposure.discal.core.logger.LOGGER +import org.dreamexposure.discal.core.`object`.new.CalendarMetadata +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model +import org.springframework.stereotype.Component +import java.time.Duration +import java.time.Instant + +@Component +class GoogleAuthService( + private val credentialService: CredentialService, + private val calendarService: CalendarService, + private val googleCalendarApiWrapper: GoogleCalendarApiWrapper, +) { + suspend fun requestNewAccessToken(calendar: CalendarMetadata): TokenV1Model? { + if (!calendar.secrets.expiresAt.isExpiredTtl()) return TokenV1Model(calendar.secrets.accessToken, calendar.secrets.expiresAt) + + LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number) + + val refreshed = googleCalendarApiWrapper.refreshAccessToken(calendar.secrets.refreshToken).entity ?: return null + calendar.secrets.accessToken = refreshed.accessToken + calendar.secrets.expiresAt = Instant.now().plusSeconds(refreshed.expiresIn.toLong()).minus(Duration.ofMinutes(5)) // Add some wiggle room + calendarService.updateCalendarMetadata(calendar) + + LOGGER.debug("Refreshed access token | guildId:{} | calendar:{}, validUntil:{}", calendar.guildId, calendar.number, calendar.external) + + return TokenV1Model(calendar.secrets.accessToken, calendar.secrets.expiresAt) + } + + suspend fun requestNewAccessToken(credentialId: Int): TokenV1Model { + val credential = credentialService.getCredential(credentialId) ?: throw NotFoundException() + if (!credential.expiresAt.isExpiredTtl()) return TokenV1Model(credential.accessToken, credential.expiresAt) + + LOGGER.debug("Refreshing access token | credentialId:$credentialId") + + val refreshed = googleCalendarApiWrapper.refreshAccessToken(credential.refreshToken).entity ?: throw EmptyNotAllowedException() + credential.accessToken = refreshed.accessToken + credential.expiresAt = Instant.now().plusSeconds(refreshed.expiresIn.toLong()).minus(Duration.ofMinutes(5)) // Add some wiggle room + credentialService.updateCredential(credential) + + LOGGER.debug("Refreshed access token | credentialId:{} | validUntil:{}", credentialId, credential.expiresAt) + + return TokenV1Model(credential.accessToken, credential.expiresAt) + } +} diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/SecurityController.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/SecurityController.kt index 69b88194c..6a49dc5df 100644 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/SecurityController.kt +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/SecurityController.kt @@ -2,10 +2,10 @@ package org.dreamexposure.discal.cam.controllers.v1 import org.dreamexposure.discal.cam.business.SecurityService import org.dreamexposure.discal.core.annotations.SecurityRequirement +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Request +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Response import org.dreamexposure.discal.core.`object`.new.security.Scope.INTERNAL_CAM_VALIDATE_TOKEN import org.dreamexposure.discal.core.`object`.new.security.TokenType.INTERNAL -import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateRequest -import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateResponse import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody @@ -19,13 +19,13 @@ class SecurityController( ) { @SecurityRequirement(schemas = [INTERNAL], scopes = [INTERNAL_CAM_VALIDATE_TOKEN]) @PostMapping("/validate", produces = ["application/json"]) - suspend fun validate(@RequestBody request: ValidateRequest): ValidateResponse { + suspend fun validate(@RequestBody request: SecurityValidateV1Request): SecurityValidateV1Response { val result = securityService.authenticateAndAuthorizeToken( request.token, request.schemas, request.scopes, ) - return ValidateResponse(result.first == HttpStatus.OK, result.first, result.second) + return SecurityValidateV1Response(result.first == HttpStatus.OK, result.first, result.second) } } diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/TokenController.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/TokenController.kt index 3bc60536e..90bc14820 100644 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/TokenController.kt +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/controllers/v1/TokenController.kt @@ -4,7 +4,7 @@ import discord4j.common.util.Snowflake import org.dreamexposure.discal.cam.managers.CalendarAuthManager import org.dreamexposure.discal.core.annotations.SecurityRequirement import org.dreamexposure.discal.core.enums.calendar.CalendarHost -import org.dreamexposure.discal.core.`object`.network.discal.CredentialData +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model import org.dreamexposure.discal.core.`object`.new.security.Scope.CALENDAR_TOKEN_READ import org.dreamexposure.discal.core.`object`.new.security.TokenType.INTERNAL import org.springframework.web.bind.annotation.GetMapping @@ -19,7 +19,7 @@ class TokenController( ) { @SecurityRequirement(schemas = [INTERNAL], scopes = [CALENDAR_TOKEN_READ]) @GetMapping(produces = ["application/json"]) - suspend fun getToken(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): CredentialData? { + suspend fun getToken(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): TokenV1Model? { return calendarAuthManager.getCredentialData(host, id, guild) } } diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt deleted file mode 100644 index 8e1d0f190..000000000 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt +++ /dev/null @@ -1,118 +0,0 @@ -package org.dreamexposure.discal.cam.google - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.module.kotlin.readValue -import com.google.api.client.http.HttpStatusCodes.STATUS_CODE_BAD_REQUEST -import com.google.api.client.http.HttpStatusCodes.STATUS_CODE_OK -import kotlinx.coroutines.reactor.awaitSingle -import okhttp3.FormBody -import okhttp3.OkHttpClient -import okhttp3.Request -import org.dreamexposure.discal.cam.json.google.ErrorData -import org.dreamexposure.discal.cam.json.google.RefreshData -import org.dreamexposure.discal.core.business.CalendarService -import org.dreamexposure.discal.core.business.CredentialService -import org.dreamexposure.discal.core.config.Config -import org.dreamexposure.discal.core.exceptions.AccessRevokedException -import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException -import org.dreamexposure.discal.core.exceptions.NotFoundException -import org.dreamexposure.discal.core.extensions.isExpiredTtl -import org.dreamexposure.discal.core.logger.LOGGER -import org.dreamexposure.discal.core.`object`.network.discal.CredentialData -import org.dreamexposure.discal.core.`object`.new.Calendar -import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT -import org.springframework.stereotype.Component -import reactor.core.publisher.Mono -import reactor.core.scheduler.Schedulers -import java.time.Duration -import java.time.Instant - -@Component -class GoogleAuth( - private val credentialService: CredentialService, - private val calendarService: CalendarService, - private val objectMapper: ObjectMapper, - private val httpClient: OkHttpClient, -) { - - suspend fun requestNewAccessToken(calendar: Calendar): CredentialData? { - if (!calendar.secrets.expiresAt.isExpiredTtl()) return CredentialData(calendar.secrets.accessToken, calendar.secrets.expiresAt) - - LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number) - - val refreshedCredential = doAccessTokenRequest(calendar.secrets.refreshToken) ?: return null - calendar.secrets.accessToken = refreshedCredential.accessToken - calendar.secrets.expiresAt = refreshedCredential.validUntil.minus(Duration.ofMinutes(5)) // Add some wiggle room - calendarService.updateCalendar(calendar) - - LOGGER.debug("Refreshing access token | guildId:{} | calendar:{}", calendar.guildId, calendar.number) - - return refreshedCredential - } - - suspend fun requestNewAccessToken(credentialId: Int): CredentialData { - val credential = credentialService.getCredential(credentialId) ?: throw NotFoundException() - if (!credential.expiresAt.isExpiredTtl()) return CredentialData(credential.accessToken, credential.expiresAt) - - LOGGER.debug("Refreshing access token | credentialId:$credentialId") - - val refreshedCredentialData = doAccessTokenRequest(credential.refreshToken) ?: throw EmptyNotAllowedException() - credential.accessToken = refreshedCredentialData.accessToken - credential.expiresAt = refreshedCredentialData.validUntil.minus(Duration.ofMinutes(5)) // Add some wiggle room - credentialService.updateCredential(credential) - - LOGGER.debug("Refreshed access token | credentialId:{} | validUntil{}", credentialId, credential.expiresAt) - - return refreshedCredentialData - } - - private suspend fun doAccessTokenRequest(refreshToken: String): CredentialData? { - val requestFormBody = FormBody.Builder() - .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString()) - .addEncoded("client_secret", Config.SECRET_GOOGLE_CLIENT_SECRET.getString()) - .addEncoded("refresh_token", refreshToken) - .addEncoded("grant_type", "refresh_token") - .build() - val request = Request.Builder() - .url("https://www.googleapis.com/oauth2/v4/token") - .post(requestFormBody) - .header("Content-Type", "application/x-www-form-urlencoded") - .build() - - - val response = Mono.fromCallable(httpClient.newCall(request)::execute) - .subscribeOn(Schedulers.boundedElastic()) - .awaitSingle() - - return when (response.code) { - STATUS_CODE_OK -> { - val body = objectMapper.readValue(response.body!!.string()) - response.close() - - CredentialData(body.accessToken, Instant.now().plusSeconds(body.expiresIn.toLong())) - } - STATUS_CODE_BAD_REQUEST -> { - val bodyRaw = response.body!!.string() - LOGGER.error("[Google] Access Token Request: $bodyRaw") - val body = objectMapper.readValue(bodyRaw) - response.close() - - - if (body.error == "invalid_grant") { - LOGGER.debug(DEFAULT, "[Google] Access to resource has been revoked") - throw AccessRevokedException() // TODO: How should I handle this for external calendars? Right now we just delete everything - } else { - LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} | ${response.message} | $body") - null - } - } - else -> { - // Failed to get OK. Send error info - LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} ${response.message} | ${response.body?.string()}") - response.close() - - null - } - } - } -} diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt deleted file mode 100644 index 73a3f2c4a..000000000 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/ErrorData.kt +++ /dev/null @@ -1,6 +0,0 @@ -package org.dreamexposure.discal.cam.json.google - - -data class ErrorData( - val error: String -) diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt deleted file mode 100644 index 870da416b..000000000 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/json/google/RefreshData.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.dreamexposure.discal.cam.json.google - -import com.fasterxml.jackson.annotation.JsonProperty - -data class RefreshData( - @JsonProperty("access_token") - val accessToken: String, - - @JsonProperty("expires_in") - val expiresIn: Int -) diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt index 79a71be2a..66ff02394 100644 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt @@ -1,29 +1,29 @@ package org.dreamexposure.discal.cam.managers import discord4j.common.util.Snowflake -import org.dreamexposure.discal.cam.google.GoogleAuth +import org.dreamexposure.discal.cam.business.google.GoogleAuthService import org.dreamexposure.discal.core.business.CalendarService import org.dreamexposure.discal.core.enums.calendar.CalendarHost import org.dreamexposure.discal.core.logger.LOGGER -import org.dreamexposure.discal.core.`object`.network.discal.CredentialData +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model import org.springframework.stereotype.Component @Component class CalendarAuthManager( private val calendarService: CalendarService, - private val googleAuth: GoogleAuth, + private val googleAuthService: GoogleAuthService, ) { - suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): CredentialData? { + suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): TokenV1Model? { return try { when (host) { CalendarHost.GOOGLE -> { if (guild == null) { // Internal (owned by DisCal, should never go bad) - googleAuth.requestNewAccessToken(id) + googleAuthService.requestNewAccessToken(id) } else { // External (owned by user) - val calendar = calendarService.getCalendar(guild, id) ?: return null - googleAuth.requestNewAccessToken(calendar) + val calendar = calendarService.getCalendarMetadata(guild, id) ?: return null + googleAuthService.requestNewAccessToken(calendar) } } } diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt index d0288c1e9..b59e4ac61 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/CalendarService.kt @@ -4,44 +4,47 @@ import discord4j.common.util.Snowflake import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingleOrNull import kotlinx.coroutines.reactor.mono -import org.dreamexposure.discal.CalendarCache +import org.dreamexposure.discal.CalendarMetadataCache import org.dreamexposure.discal.core.crypto.AESEncryption -import org.dreamexposure.discal.core.database.CalendarRepository -import org.dreamexposure.discal.core.database.DatabaseManager -import org.dreamexposure.discal.core.`object`.new.Calendar +import org.dreamexposure.discal.core.database.CalendarMetadataRepository +import org.dreamexposure.discal.core.`object`.new.CalendarMetadata import org.springframework.stereotype.Component @Component class CalendarService( - private val calendarRepository: CalendarRepository, - private val calendarCache: CalendarCache, + private val calendarMetadataRepository: CalendarMetadataRepository, + private val calendarMetadataCache: CalendarMetadataCache, private val settingsService: GuildSettingsService, ) { - suspend fun getCalendarCount(): Long = calendarRepository.countAll().awaitSingle() + suspend fun getCalendarCount(): Long = calendarMetadataRepository.countAll().awaitSingle() - suspend fun getAllCalendars(guildId: Snowflake): List { - var calendars = calendarCache.get(key = guildId)?.toList() + suspend fun getCalendarCount(guildId: Snowflake) = calendarMetadataRepository.countAllByGuildId(guildId.asLong()).awaitSingle() + + // TODO: Exposing CalendarMetadata directly should not be done once a higher abstraction has been implemented + suspend fun getAllCalendarMetadata(guildId: Snowflake): List { + var calendars = calendarMetadataCache.get(key = guildId)?.toList() if (calendars != null) return calendars - calendars = calendarRepository.findAllByGuildId(guildId.asLong()) - .flatMap { mono { Calendar(it) } } + calendars = calendarMetadataRepository.findAllByGuildId(guildId.asLong()) + .flatMap { mono { CalendarMetadata(it) } } .collectList() .awaitSingle() - calendarCache.put(key = guildId, value = calendars.toTypedArray()) + calendarMetadataCache.put(key = guildId, value = calendars.toTypedArray()) return calendars } - suspend fun getCalendar(guildId: Snowflake, number: Int): Calendar? { - return getAllCalendars(guildId).firstOrNull { it.number == number } + suspend fun getCalendarMetadata(guildId: Snowflake, number: Int): CalendarMetadata? { + return getAllCalendarMetadata(guildId).firstOrNull { it.number == number } } - suspend fun updateCalendar(calendar: Calendar) { + // TODO: This should be privated once a higher abstraction has been implemented + suspend fun updateCalendarMetadata(calendar: CalendarMetadata) { val aes = AESEncryption(calendar.secrets.privateKey) val encryptedRefreshToken = aes.encrypt(calendar.secrets.refreshToken).awaitSingle() val encryptedAccessToken = aes.encrypt(calendar.secrets.accessToken).awaitSingle() - calendarRepository.updateCalendarByGuildIdAndCalendarNumber( + calendarMetadataRepository.updateCalendarByGuildIdAndCalendarNumber( guildId = calendar.guildId.asLong(), calendarNumber = calendar.number, host = calendar.host.name, @@ -55,18 +58,17 @@ class CalendarService( expiresAt = calendar.secrets.expiresAt.toEpochMilli(), ).awaitSingleOrNull() - val cached = calendarCache.get(key = calendar.guildId) + val cached = calendarMetadataCache.get(key = calendar.guildId) if (cached != null) { val newList = cached.toMutableList() newList.removeIf { it.number == calendar.number } - calendarCache.put(key = calendar.guildId,value = (newList + calendar).toTypedArray()) + calendarMetadataCache.put(key = calendar.guildId,value = (newList + calendar).toTypedArray()) } } suspend fun canAddNewCalendar(guildId: Snowflake): Boolean { - // For compatibility with legacy system - val calCount = DatabaseManager.getCalendarCount(guildId).awaitSingle() - if (calCount == 0) return true + val calCount = getCalendarCount(guildId) + if (calCount == 0L) return true val settings = settingsService.getSettings(guildId) return calCount < settings.maxCalendars diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/CamApiWrapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/CamApiWrapper.kt new file mode 100644 index 000000000..41c2cea25 --- /dev/null +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/CamApiWrapper.kt @@ -0,0 +1,106 @@ +package org.dreamexposure.discal.core.business.api + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import discord4j.common.util.Snowflake +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import okhttp3.Response +import okhttp3.executeAsync +import org.dreamexposure.discal.core.config.Config +import org.dreamexposure.discal.core.enums.calendar.CalendarHost +import org.dreamexposure.discal.core.logger.LOGGER +import org.dreamexposure.discal.core.`object`.new.model.ResponseModel +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Request +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Response +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.TokenV1Model +import org.dreamexposure.discal.core.`object`.rest.ErrorResponse +import org.dreamexposure.discal.core.utils.GlobalVal.JSON +import org.springframework.stereotype.Component + +@Component +abstract class CamApiWrapper( + private val httpClient: OkHttpClient, + private val objectMapper: ObjectMapper, +) { + private final val CAM_URL = Config.URL_CAM.getString() + private final val AUTH_HEADER = "Int ${Config.SECRET_DISCAL_API_KEY.getString()}" + + suspend fun validateToken(requestBody: SecurityValidateV1Request): ResponseModel { + val request = Request.Builder() + .url("${Config.URL_CAM.getString()}/v1/security/validate") + .post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON)) + .header("Authorization", AUTH_HEADER) + .header("Content-Type", "application/json") + .build() + + return makeRequest(request, SecurityValidateV1Response::class.java) + } + + suspend fun getCalendarToken(credentialId: Int): ResponseModel { + LOGGER.debug("Getting calendar token for credential:$credentialId") + + val url = "$CAM_URL/v1/token".toHttpUrl().newBuilder() + .addQueryParameter("host", CalendarHost.GOOGLE.name) + .addQueryParameter("id", credentialId.toString()) + .build() + + val request = Request.Builder().get() + .header("Authorization", AUTH_HEADER) + .url(url) + .build() + + return makeRequest(request, TokenV1Model::class.java) + } + + suspend fun getCalendarToken(guildId: Snowflake, calNumber: Int, host: CalendarHost): ResponseModel { + LOGGER.debug("Getting calendar token for guild:{} | host:{} | calendarId:{} ", guildId.asLong(), host.name, calNumber) + + val url = "$CAM_URL/v1/token".toHttpUrl().newBuilder() + .addQueryParameter("host", host.name) + .addQueryParameter("guild", guildId.asString()) + .addQueryParameter("id", calNumber.toString()) + .build() + + val request = Request.Builder().get() + .header("Authorization", AUTH_HEADER) + .url(url) + .build() + + return makeRequest(request, TokenV1Model::class.java) + } + + private suspend fun makeRequest(request: Request, valueType: Class): ResponseModel { + var response: Response? = null + + try { + response = httpClient.newCall(request).executeAsync() + + when (response.code) { + 200 -> { + val data = objectMapper.readValue(response.body!!.string(), valueType) + response.body?.close() + response.close() + + return ResponseModel(data) + } + else -> { + val error = objectMapper.readValue(response.body!!.string()) + response.body?.close() + response.close() + + return ResponseModel(error, response.code) + } + } + + } catch (ex: Exception) { + LOGGER.error("Error making request host:${request.url.host} | uri:${request.url.encodedPath} | code:${response?.code}", ex) + throw ex // Rethrow and let implementation decide proper handling for exception + } finally { + response?.body?.close() + response?.close() + } + } +} diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/GoogleCalendarApiWrapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/GoogleCalendarApiWrapper.kt new file mode 100644 index 000000000..087a85d25 --- /dev/null +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/business/api/GoogleCalendarApiWrapper.kt @@ -0,0 +1,78 @@ +package org.dreamexposure.discal.core.business.api + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import okhttp3.* +import org.dreamexposure.discal.core.config.Config +import org.dreamexposure.discal.core.exceptions.AccessRevokedException +import org.dreamexposure.discal.core.logger.LOGGER +import org.dreamexposure.discal.core.`object`.new.model.ResponseModel +import org.dreamexposure.discal.core.`object`.new.model.google.OauthV4RefreshTokenResponse +import org.dreamexposure.discal.core.`object`.rest.ErrorResponse +import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT +import org.springframework.stereotype.Component + +@Component +class GoogleCalendarApiWrapper( + private val httpClient: OkHttpClient, + private val objectMapper: ObjectMapper, +) { + suspend fun refreshAccessToken(refreshToken: String): ResponseModel { + val requestFormBody = FormBody.Builder() + .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString()) + .addEncoded("client_secret", Config.SECRET_GOOGLE_CLIENT_SECRET.getString()) + .addEncoded("refresh_token", refreshToken) + .addEncoded("grant_type", "refresh_token") + .build() + val request = Request.Builder() + .url("https://www.googleapis.com/oauth2/v4/token") + .post(requestFormBody) + .header("Content-Type", "application/x-www-form-urlencoded") + .build() + + val response = makeRequest(request, OauthV4RefreshTokenResponse::class.java) + + + // TODO: Handling of this should be moved up higher in the impl? + if (response.error?.error == "invalid_grant") { + LOGGER.debug(DEFAULT, "Google Oauth invalid_grant for access token refresh") + throw AccessRevokedException() // TODO: How should I handle this for external calendars? Right now we just delete everything + } else if (response.error != null) { + LOGGER.error(DEFAULT, "[Google] Error requesting new access token | ${response.code} | ${response.error.error}") + } + + return response + } + + private suspend fun makeRequest(request: Request, valueType: Class): ResponseModel { + var response: Response? = null + + try { + response = httpClient.newCall(request).executeAsync() + + when (response.code) { + 200 -> { + val data = objectMapper.readValue(response.body!!.string(), valueType) + response.body?.close() + response.close() + + return ResponseModel(data) + } + else -> { + val error = objectMapper.readValue(response.body!!.string()) + response.body?.close() + response.close() + + return ResponseModel(error, response.code) + } + } + + } catch (ex: Exception) { + LOGGER.error("Error making request host:${request.url.host} | uri:${request.url.encodedPath} | code:${response?.code}", ex) + throw ex // Rethrow and let implementation decide proper handling for exception + } finally { + response?.body?.close() + response?.close() + } + } +} diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt index 63c535e30..951b11f37 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/config/CacheConfig.kt @@ -45,7 +45,7 @@ class CacheConfig { @Bean @Primary @ConditionalOnProperty("bot.cache.redis", havingValue = "true") - fun calendarRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): CalendarCache = + fun calendarMetadataRedisCache(objectMapper: ObjectMapper, redisTemplate: ReactiveStringRedisTemplate): CalendarMetadataCache = RedisStringCacheRepository(objectMapper, redisTemplate, "Calendars", calendarTtl) @Bean @@ -84,7 +84,7 @@ class CacheConfig { fun oauthStateFallbackCache(): OauthStateCache = JdkCacheRepository(oauthStateTtl) @Bean - fun calendarFallbackCache(): CalendarCache = JdkCacheRepository(calendarTtl) + fun calendarMetadataFallbackCache(): CalendarMetadataCache = JdkCacheRepository(calendarTtl) @Bean fun rsvpFallbackCache(): RsvpCache = JdkCacheRepository(rsvpTtl) diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataData.kt similarity index 92% rename from core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt rename to core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataData.kt index 4e895c714..673611c68 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarData.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataData.kt @@ -3,7 +3,7 @@ package org.dreamexposure.discal.core.database import org.springframework.data.relational.core.mapping.Table @Table("calendars") -data class CalendarData( +data class CalendarMetadataData( val guildId: Long, val calendarNumber: Int, val host: String, diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataRepository.kt similarity index 83% rename from core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt rename to core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataRepository.kt index 6a6565bc7..9cb78f814 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarRepository.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/database/CalendarMetadataRepository.kt @@ -5,14 +5,16 @@ import org.springframework.data.r2dbc.repository.R2dbcRepository import reactor.core.publisher.Flux import reactor.core.publisher.Mono -interface CalendarRepository : R2dbcRepository { +interface CalendarMetadataRepository : R2dbcRepository { @Query("SELECT COUNT(*) FROM calendars") fun countAll(): Mono - fun findAllByGuildId(guildId: Long): Flux + fun countAllByGuildId(guildId: Long): Mono - fun findByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono + fun findAllByGuildId(guildId: Long): Flux + + fun findByGuildIdAndCalendarNumber(guildId: Long, calendarNumber: Int): Mono @Query(""" UPDATE calendars diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/CredentialData.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/CredentialData.kt index ab7028ad1..7ab121431 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/CredentialData.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/network/discal/CredentialData.kt @@ -6,6 +6,7 @@ import org.dreamexposure.discal.core.serializers.InstantAsStringSerializer import java.time.Instant @Serializable +@Deprecated("Really shouldn't need this after the auth wrapper has been redone") data class CredentialData( @SerialName("access_token") val accessToken: String, diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/CalendarMetadata.kt similarity index 82% rename from core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt rename to core/src/main/kotlin/org/dreamexposure/discal/core/object/new/CalendarMetadata.kt index 61377fc9b..88d66106e 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/Calendar.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/CalendarMetadata.kt @@ -4,8 +4,7 @@ import discord4j.common.util.Snowflake import kotlinx.coroutines.reactor.awaitSingle import kotlinx.coroutines.reactor.awaitSingleOrNull import org.dreamexposure.discal.core.crypto.AESEncryption -import org.dreamexposure.discal.core.database.CalendarData -import org.dreamexposure.discal.core.enums.calendar.CalendarHost +import org.dreamexposure.discal.core.database.CalendarMetadataData import org.dreamexposure.discal.core.extensions.asInstantMilli import org.dreamexposure.discal.core.extensions.asSnowflake import org.dreamexposure.discal.core.extensions.isExpiredTtl @@ -13,17 +12,17 @@ import reactor.core.publisher.Mono import java.time.Instant import javax.crypto.IllegalBlockSizeException -data class Calendar( +data class CalendarMetadata( val guildId: Snowflake, val number: Int, - val host: CalendarHost, + val host: Host, val id: String, val address: String, val external: Boolean, val secrets: Secrets, ) { companion object { - suspend operator fun invoke(data: CalendarData): Calendar { + suspend operator fun invoke(data: CalendarMetadataData): CalendarMetadata { val aes = AESEncryption(data.privateKey) val accessToken = if (!data.expiresAt.asInstantMilli().isExpiredTtl()) aes.decrypt(data.accessToken) @@ -32,10 +31,10 @@ data class Calendar( }.awaitSingleOrNull() else null // No point in trying to decrypt if it's expired - return Calendar( + return CalendarMetadata( guildId = data.guildId.asSnowflake(), number = data.calendarNumber, - host = CalendarHost.valueOf(data.host), + host = Host.valueOf(data.host), id = data.calendarId, address = data.calendarAddress, external = data.external, @@ -50,6 +49,9 @@ data class Calendar( } } + //////////////////////////// + ////// Nested classes ////// + //////////////////////////// data class Secrets( val credentialId: Int, val privateKey: String, @@ -57,4 +59,8 @@ data class Calendar( var refreshToken: String, var accessToken: String, ) + + enum class Host { + GOOGLE, + } } diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/ResponseModel.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/ResponseModel.kt new file mode 100644 index 000000000..428d8b1df --- /dev/null +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/ResponseModel.kt @@ -0,0 +1,13 @@ +package org.dreamexposure.discal.core.`object`.new.model + +import org.dreamexposure.discal.core.`object`.rest.ErrorResponse + +data class ResponseModel( + val code: Int, + val entity: T?, + val error: ErrorResponse? +) { + constructor(entity: T, code: Int = 200): this(code, entity, null) + + constructor(error: ErrorResponse, code: Int): this(code, null, error) +} diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateRequest.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Request.kt similarity index 67% rename from core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateRequest.kt rename to core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Request.kt index f2fc5fa64..01dce5461 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateRequest.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Request.kt @@ -1,9 +1,9 @@ -package org.dreamexposure.discal.core.`object`.rest.v1.security +package org.dreamexposure.discal.core.`object`.new.model.discal.cam import org.dreamexposure.discal.core.`object`.new.security.Scope import org.dreamexposure.discal.core.`object`.new.security.TokenType -data class ValidateRequest( +data class SecurityValidateV1Request( val token: String, val schemas: List, val scopes: List, diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateResponse.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Response.kt similarity index 53% rename from core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateResponse.kt rename to core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Response.kt index bae346c37..947461b35 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/v1/security/ValidateResponse.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/SecurityValidateV1Response.kt @@ -1,9 +1,10 @@ -package org.dreamexposure.discal.core.`object`.rest.v1.security +package org.dreamexposure.discal.core.`object`.new.model.discal.cam import org.springframework.http.HttpStatus -data class ValidateResponse( +data class SecurityValidateV1Response( val valid: Boolean, val code: HttpStatus, val message: String, ) + diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/TokenV1Model.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/TokenV1Model.kt new file mode 100644 index 000000000..654cae4e8 --- /dev/null +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/discal/cam/TokenV1Model.kt @@ -0,0 +1,8 @@ +package org.dreamexposure.discal.core.`object`.new.model.discal.cam + +import java.time.Instant + +data class TokenV1Model( + val accessToken: String, + val validUntil: Instant, +) diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/google/OauthV4RefreshTokenResponse.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/google/OauthV4RefreshTokenResponse.kt new file mode 100644 index 000000000..9d6d61444 --- /dev/null +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/new/model/google/OauthV4RefreshTokenResponse.kt @@ -0,0 +1,11 @@ +package org.dreamexposure.discal.core.`object`.new.model.google + +import com.fasterxml.jackson.annotation.JsonProperty + +data class OauthV4RefreshTokenResponse( + @JsonProperty("access_token") + val accessToken: String, + + @JsonProperty("expires_in") + val expiresIn: Int +) diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/ErrorResponse.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/ErrorResponse.kt index 213bbb613..85f73e858 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/ErrorResponse.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/object/rest/ErrorResponse.kt @@ -1,5 +1,5 @@ package org.dreamexposure.discal.core.`object`.rest data class ErrorResponse( - val message: String, + val error: String, ) diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt index 67f1739d4..f4d405d89 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/spring/SecurityWebFilter.kt @@ -3,16 +3,11 @@ package org.dreamexposure.discal.core.spring import com.fasterxml.jackson.databind.ObjectMapper import kotlinx.coroutines.reactive.awaitFirstOrNull import kotlinx.coroutines.reactor.mono -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody import org.dreamexposure.discal.core.annotations.SecurityRequirement -import org.dreamexposure.discal.core.config.Config +import org.dreamexposure.discal.core.business.api.CamApiWrapper import org.dreamexposure.discal.core.extensions.spring.writeJsonString +import org.dreamexposure.discal.core.`object`.new.model.discal.cam.SecurityValidateV1Request import org.dreamexposure.discal.core.`object`.rest.ErrorResponse -import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateRequest -import org.dreamexposure.discal.core.`object`.rest.v1.security.ValidateResponse -import org.dreamexposure.discal.core.utils.GlobalVal.JSON import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty import org.springframework.http.HttpStatus import org.springframework.stereotype.Component @@ -26,8 +21,8 @@ import reactor.core.publisher.Mono @Component @ConditionalOnProperty(name = ["discal.security.enabled"], havingValue = "true") class SecurityWebFilter( + private val camApiWrapper: CamApiWrapper, private val handlerMapping: RequestMappingHandlerMapping, - private val httpClient: OkHttpClient, private val objectMapper: ObjectMapper, ) : WebFilter { override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono { @@ -68,30 +63,20 @@ class SecurityWebFilter( } // Use CAM to validate token - val requestBody = ValidateRequest(authHeader, authAnnotation.schemas.toList(), authAnnotation.scopes.toList()) - val request = Request.Builder() - .url("${Config.URL_CAM.getString()}/v1/security/validate") - .post(objectMapper.writeValueAsString(requestBody).toRequestBody(JSON)) - .header("Authorization", "Int ${Config.SECRET_DISCAL_API_KEY.getString()}") - .header("Content-Type", "application/json") - .build() + val requestBody = SecurityValidateV1Request(authHeader, authAnnotation.schemas.toList(), authAnnotation.scopes.toList()) - val response = httpClient.newCall(request).execute() - if (response.isSuccessful) { - val responseBody = objectMapper.readValue(response.body!!.string(), ValidateResponse::class.java) - response.close() + val response = camApiWrapper.validateToken(requestBody) - if (!responseBody.valid) { - exchange.response.statusCode = responseBody.code - exchange.response.writeJsonString( - objectMapper.writeValueAsString(ErrorResponse(responseBody.message)) - ).awaitFirstOrNull() - return - } - } else { - val responseBody = objectMapper.readValue(response.body!!.string(), ErrorResponse::class.java) - response.close() - throw IllegalStateException("Failed to validate token | ${response.code} | ${responseBody.message}") + if (response.code != 200) { + throw IllegalStateException("Failed to validate token | ${response.code} | ${response.error?.error}") + } + + if (!response.entity!!.valid) { + exchange.response.statusCode = response.entity.code + exchange.response.writeJsonString( + objectMapper.writeValueAsString(ErrorResponse(response.entity.message)) + ).awaitFirstOrNull() + return } // If we made it here, everything is good to go. diff --git a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt index 8a3c630ae..0530d53b7 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/core/wrapper/google/GoogleAuthWrapper.kt @@ -5,20 +5,15 @@ import com.google.api.client.googleapis.auth.oauth2.GoogleCredential import com.google.api.client.http.HttpStatusCodes import com.google.api.client.http.javanet.NetHttpTransport import com.google.api.client.json.gson.GsonFactory -import com.google.api.services.calendar.CalendarScopes import discord4j.common.util.Snowflake -import okhttp3.FormBody import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.Request -import okhttp3.Response import org.dreamexposure.discal.core.config.Config import org.dreamexposure.discal.core.database.DatabaseManager import org.dreamexposure.discal.core.enums.calendar.CalendarHost import org.dreamexposure.discal.core.exceptions.EmptyNotAllowedException -import org.dreamexposure.discal.core.exceptions.google.GoogleAuthCancelException import org.dreamexposure.discal.core.logger.LOGGER import org.dreamexposure.discal.core.`object`.calendar.CalendarData -import org.dreamexposure.discal.core.`object`.google.GoogleAuthPoll import org.dreamexposure.discal.core.`object`.network.discal.CredentialData import org.dreamexposure.discal.core.`object`.rest.RestError import org.dreamexposure.discal.core.utils.GlobalVal.DEFAULT @@ -26,7 +21,6 @@ import org.dreamexposure.discal.core.utils.GlobalVal.HTTP_CLIENT import org.dreamexposure.discal.core.utils.GlobalVal.JSON_FORMAT import reactor.core.publisher.Mono import reactor.core.scheduler.Schedulers -import java.time.Duration import java.util.concurrent.ConcurrentHashMap import kotlin.random.Random import com.google.api.services.calendar.Calendar as GoogleCalendarService @@ -171,50 +165,4 @@ object GoogleAuthWrapper { } fun randomCredentialId() = Random.nextInt(Config.SECRET_GOOGLE_CREDENTIAL_COUNT.getInt()) - - fun requestDeviceCode(): Mono { - return Mono.fromCallable { - val body = FormBody.Builder() - .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString()) - .addEncoded("scope", CalendarScopes.CALENDAR) - .build() - - val request = Request.Builder() - .url("https://accounts.google.com/o/oauth2/device/code") - .post(body) - .header("Content-Type", "application/x-www-form-urlencoded") - .build() - - HTTP_CLIENT.newCall(request).execute() - }.subscribeOn(Schedulers.boundedElastic()) - } - - fun requestPollResponse(poll: GoogleAuthPoll): Mono { - return Mono.fromCallable { - val body = FormBody.Builder() - .addEncoded("client_id", Config.SECRET_GOOGLE_CLIENT_ID.getString()) - .addEncoded("client_secret", Config.SECRET_GOOGLE_CLIENT_SECRET.getString()) - .addEncoded("code", poll.deviceCode) - .addEncoded("grant_type", "http://oauth.net/grant_type/device/1.0") - .build() - - val request = Request.Builder() - .url("https://www.googleapis.com/oauth2/v4/token") - .post(body) - .header("Content-Type", "application/x-www-form-urlencoded") - .build() - - HTTP_CLIENT.newCall(request).execute() - }.subscribeOn(Schedulers.boundedElastic()) - } - - fun scheduleOAuthPoll(poll: GoogleAuthPoll): Mono { - poll.remainingSeconds = poll.remainingSeconds - poll.interval - - return poll.callback(poll) - .then(Mono.delay(Duration.ofSeconds(poll.interval.toLong()))) - .repeat() - .then() - .onErrorResume(GoogleAuthCancelException::class.java) { Mono.empty() } - } } diff --git a/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt b/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt index 68a5765c0..9524b4a70 100644 --- a/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt +++ b/core/src/main/kotlin/org/dreamexposure/discal/typealiases.kt @@ -8,7 +8,7 @@ import org.dreamexposure.discal.core.`object`.new.* typealias GuildSettingsCache = CacheRepository typealias CredentialsCache = CacheRepository typealias OauthStateCache = CacheRepository -typealias CalendarCache = CacheRepository> +typealias CalendarMetadataCache = CacheRepository> typealias RsvpCache = CacheRepository typealias StaticMessageCache = CacheRepository typealias AnnouncementCache = CacheRepository> diff --git a/gradle.properties b/gradle.properties index 31063b4e2..8793b9b87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,9 +25,9 @@ orgJsonVersion=20240303 logbackContribVersion=0.1.5 # Google libs -googleApiClientVersion=2.0.0 -googleServicesCalendarVersion=v3-rev20220715-2.0.0 -googleOauthClientVersion=1.34.1 +googleApiClientVersion=2.5.1 +googleServicesCalendarVersion=v3-rev20240419-2.0.0 +googleOauthClientVersion=1.36.0 # Various Libs okhttpVersion=5.0.0-alpha.12