Skip to content

Commit

Permalink
Cleanup calendar authentication implementation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
NovaFox161 committed May 18, 2024
1 parent 45d6508 commit cf17c30
Show file tree
Hide file tree
Showing 25 changed files with 351 additions and 273 deletions.
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
}
118 changes: 0 additions & 118 deletions cam/src/main/kotlin/org/dreamexposure/discal/cam/google/GoogleAuth.kt

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Calendar> {
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<CalendarMetadata> {
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,
Expand All @@ -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
Expand Down

0 comments on commit cf17c30

Please sign in to comment.