diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt index b152d438e..a21e653cf 100644 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/GetEndpoint.kt @@ -1,9 +1,8 @@ package org.dreamexposure.discal.cam.endpoints.v1 import discord4j.common.util.Snowflake -import org.dreamexposure.discal.cam.google.GoogleAuth +import org.dreamexposure.discal.cam.managers.CalendarAuthManager import org.dreamexposure.discal.core.annotations.Authentication -import org.dreamexposure.discal.core.business.CalendarService import org.dreamexposure.discal.core.enums.calendar.CalendarHost import org.dreamexposure.discal.core.`object`.network.discal.CredentialData import org.springframework.web.bind.annotation.GetMapping @@ -14,23 +13,11 @@ import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/v1/") class GetEndpoint( - private val calendarService: CalendarService, - private val googleAuth: GoogleAuth, + private val calendarAuthManager: CalendarAuthManager, ) { @Authentication(access = Authentication.AccessLevel.ADMIN) @GetMapping("token", produces = ["application/json"]) suspend fun get(@RequestParam host: CalendarHost, @RequestParam id: Int, @RequestParam guild: Snowflake?): CredentialData? { - return when (host) { - CalendarHost.GOOGLE -> { - if (guild == null) { - // Internal (owned by DisCal, should never go bad) - googleAuth.requestNewAccessToken(id) - } else { - // External (owned by user) - val calendar = calendarService.getCalendar(guild, id) ?: return null - googleAuth.requestNewAccessToken(calendar) - } - } - } + return calendarAuthManager.getCredentialData(host, id, guild) } } diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt index 6768872d4..54103efa5 100644 --- a/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/endpoints/v1/oauth2/DiscordOauthEndpoint.kt @@ -1,73 +1,34 @@ package org.dreamexposure.discal.cam.endpoints.v1.oauth2 -import org.dreamexposure.discal.cam.business.OauthStateService -import org.dreamexposure.discal.cam.discord.DiscordOauthHandler import org.dreamexposure.discal.cam.json.discal.LoginResponse import org.dreamexposure.discal.cam.json.discal.TokenRequest import org.dreamexposure.discal.cam.json.discal.TokenResponse +import org.dreamexposure.discal.cam.managers.DiscordOauthManager import org.dreamexposure.discal.core.annotations.Authentication -import org.dreamexposure.discal.core.business.SessionService -import org.dreamexposure.discal.core.config.Config -import org.dreamexposure.discal.core.crypto.KeyGenerator -import org.dreamexposure.discal.core.`object`.WebSession -import org.dreamexposure.discal.core.utils.GlobalVal.discordApiUrl -import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.* -import org.springframework.web.server.ResponseStatusException -import java.net.URLEncoder -import java.nio.charset.Charset.defaultCharset @RestController @RequestMapping("/oauth2/discord/") class DiscordOauthEndpoint( - private val sessionService: SessionService, - private val oauthStateService: OauthStateService, - private val discordOauthHandler: DiscordOauthHandler, + private val discordOauthManager: DiscordOauthManager, ) { - private val redirectUrl = Config.URL_DISCORD_REDIRECT.getString() - private val clientId = Config.DISCORD_APP_ID.getString() - - private final val scopes = URLEncoder.encode("identify guilds", defaultCharset()) - private final val encodedRedirectUrl = URLEncoder.encode(redirectUrl, defaultCharset()) - private final val oauthLinkWithoutState = "$discordApiUrl/oauth2/authorize?client_id=$clientId&redirect_uri=$encodedRedirectUrl&response_type=code&scope=$scopes&prompt=none" @GetMapping("login") @Authentication(access = Authentication.AccessLevel.PUBLIC) suspend fun login(): LoginResponse { - val state = oauthStateService.generateState() - - val link = "$oauthLinkWithoutState&state=$state" - + val link = discordOauthManager.getOauthLinkForLogin() return LoginResponse(link) } @GetMapping("logout") @Authentication(access = Authentication.AccessLevel.WRITE) suspend fun logout(@RequestHeader("Authorization") token: String) { - sessionService.deleteSession(token) + discordOauthManager.handleLogout(token) } @PostMapping("code") @Authentication(access = Authentication.AccessLevel.PUBLIC) suspend fun token(@RequestBody body: TokenRequest): TokenResponse { - // Validate state - if (!oauthStateService.validateState(body.state)) { - // State invalid - 400 - throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state") - } - - val dTokens = discordOauthHandler.doTokenExchange(body.code) - val authInfo = discordOauthHandler.getOauthInfo(dTokens.accessToken) - val apiToken = KeyGenerator.csRandomAlphaNumericString(64) - val session = WebSession( - apiToken, - authInfo.user!!.id, - accessToken = dTokens.accessToken, - refreshToken = dTokens.refreshToken - ) - - sessionService.removeAndInsertSession(session) - - return TokenResponse(session.token, session.expiresAt, authInfo.user) + return discordOauthManager.handleCodeExchange(body.state, body.code) } } 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 new file mode 100644 index 000000000..b337711ae --- /dev/null +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/CalendarAuthManager.kt @@ -0,0 +1,29 @@ +package org.dreamexposure.discal.cam.managers + +import discord4j.common.util.Snowflake +import org.dreamexposure.discal.cam.google.GoogleAuth +import org.dreamexposure.discal.core.business.CalendarService +import org.dreamexposure.discal.core.enums.calendar.CalendarHost +import org.dreamexposure.discal.core.`object`.network.discal.CredentialData +import org.springframework.stereotype.Component + +@Component +class CalendarAuthManager( + private val calendarService: CalendarService, + private val googleAuth: GoogleAuth, +) { + suspend fun getCredentialData(host: CalendarHost, id: Int, guild: Snowflake?): CredentialData? { + return when (host) { + CalendarHost.GOOGLE -> { + if (guild == null) { + // Internal (owned by DisCal, should never go bad) + googleAuth.requestNewAccessToken(id) + } else { + // External (owned by user) + val calendar = calendarService.getCalendar(guild, id) ?: return null + googleAuth.requestNewAccessToken(calendar) + } + } + } + } +} diff --git a/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt new file mode 100644 index 000000000..e553c88b6 --- /dev/null +++ b/cam/src/main/kotlin/org/dreamexposure/discal/cam/managers/DiscordOauthManager.kt @@ -0,0 +1,63 @@ +package org.dreamexposure.discal.cam.managers + +import org.dreamexposure.discal.cam.business.OauthStateService +import org.dreamexposure.discal.cam.discord.DiscordOauthHandler +import org.dreamexposure.discal.cam.json.discal.TokenResponse +import org.dreamexposure.discal.core.business.SessionService +import org.dreamexposure.discal.core.config.Config +import org.dreamexposure.discal.core.crypto.KeyGenerator +import org.dreamexposure.discal.core.`object`.WebSession +import org.dreamexposure.discal.core.utils.GlobalVal.discordApiUrl +import org.springframework.http.HttpStatus +import org.springframework.stereotype.Component +import org.springframework.web.server.ResponseStatusException +import java.net.URLEncoder +import java.nio.charset.Charset.defaultCharset + +@Component +class DiscordOauthManager( + private val sessionService: SessionService, + private val oauthStateService: OauthStateService, + private val discordOauthHandler: DiscordOauthHandler, +) { + private final val redirectUrl = Config.URL_DISCORD_REDIRECT.getString() + private final val clientId = Config.DISCORD_APP_ID.getString() + + private final val scopes = URLEncoder.encode("identify guilds", defaultCharset()) + private final val encodedRedirectUrl = URLEncoder.encode(redirectUrl, defaultCharset()) + private final val oauthLinkWithoutState = "$discordApiUrl/oauth2/authorize?client_id=$clientId&redirect_uri=$encodedRedirectUrl&response_type=code&scope=$scopes&prompt=none" + + suspend fun getOauthLinkForLogin(): String { + val state = oauthStateService.generateState() + + return "$oauthLinkWithoutState&state=$state" + } + + suspend fun handleLogout(token: String) { + sessionService.deleteSession(token) + } + + suspend fun handleCodeExchange(state: String, code: String): TokenResponse { + // Validate state + if (!oauthStateService.validateState(state)) { + // State invalid - 400 + throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid state") + } + + val dTokens = discordOauthHandler.doTokenExchange(code) + val authInfo = discordOauthHandler.getOauthInfo(dTokens.accessToken) + val apiToken = KeyGenerator.csRandomAlphaNumericString(64) + val session = WebSession( + apiToken, + authInfo.user!!.id, + accessToken = dTokens.accessToken, + refreshToken = dTokens.refreshToken + ) + + sessionService.removeAndInsertSession(session) + + return TokenResponse(session.token, session.expiresAt, authInfo.user) + + TODO() + } +}