diff --git a/extensions/intellij/build.gradle.kts b/extensions/intellij/build.gradle.kts index fb85ded90b..e71adfd369 100644 --- a/extensions/intellij/build.gradle.kts +++ b/extensions/intellij/build.gradle.kts @@ -34,9 +34,6 @@ repositories { // Dependencies are managed with Gradle version catalog - read more: // https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog dependencies { - implementation("com.squareup.okhttp3:okhttp:4.12.0") { - exclude(group = "org.jetbrains.kotlin", module = "kotlin-stdlib") - } implementation("org.jetbrains.kotlin:kotlin-stdlib:1.4.32") implementation("com.posthog.java:posthog:1.+") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0") diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt index 13ceaf8107..4538b87713 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/auth/ContinueAuthService.kt @@ -1,5 +1,6 @@ package com.github.continuedev.continueintellijextension.auth +import com.github.continuedev.continueintellijextension.error.ContinueErrorService import com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings import com.github.continuedev.continueintellijextension.services.ContinuePluginService import com.google.gson.Gson @@ -10,24 +11,22 @@ import com.intellij.ide.util.PropertiesComponent import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.components.Service import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.remoteServer.util.CloudConfigurationUtil.createCredentialAttributes +import com.intellij.util.io.HttpRequests import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.MediaType.Companion.toMediaType -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.RequestBody.Companion.toRequestBody -import java.net.URL +import kotlin.time.Duration.Companion.minutes @Service class ContinueAuthService { private val coroutineScope = CoroutineScope(Dispatchers.IO) + private val log = Logger.getInstance(ContinueAuthService::class.java) companion object { - fun getInstance(): ContinueAuthService = service() private const val CREDENTIALS_USER = "ContinueAuthUser" private const val ACCESS_TOKEN_KEY = "ContinueAccessToken" private const val REFRESH_TOKEN_KEY = "ContinueRefreshToken" @@ -77,73 +76,47 @@ class ContinueAuthService { } private fun updateRefreshToken(token: String) { - // Launch a coroutine to call the suspend function - coroutineScope.launch { - try { - val response = refreshToken(token) - val accessToken = response["accessToken"] as? String - val refreshToken = response["refreshToken"] as? String - val user = response["user"] as? Map<*, *> - val firstName = user?.get("firstName") as? String - val lastName = user?.get("lastName") as? String - val label = "$firstName $lastName" - val id = user?.get("id") as? String - val email = user?.get("email") as? String - - // Persist the session info - setRefreshToken(refreshToken!!) - val sessionInfo = - ControlPlaneSessionInfo(accessToken!!, ControlPlaneSessionInfo.Account(email!!, label)) - setControlPlaneSessionInfo(sessionInfo) - - // Notify listeners - ApplicationManager.getApplication().messageBus.syncPublisher(AuthListener.TOPIC) - .handleUpdatedSessionInfo(sessionInfo) - - } catch (e: Exception) { - // Handle any exceptions - println("Exception while refreshing token: ${e.message}") - } + try { + val response = refreshToken(token) + val accountLabel = "${response.user.firstName} ${response.user.lastName}" + + // Persist the session info + setRefreshToken(response.refreshToken) + val account = ControlPlaneSessionInfo.Account(response.user.email, accountLabel) + val sessionInfo = ControlPlaneSessionInfo(response.accessToken, account) + setControlPlaneSessionInfo(sessionInfo) + + // Notify listeners + ApplicationManager.getApplication().messageBus.syncPublisher(AuthListener.TOPIC) + .handleUpdatedSessionInfo(sessionInfo) + } catch (e: Exception) { + service().report(e, "Exception while refreshing token ${e.message}") } } private fun setupRefreshTokenInterval() { - // Launch a coroutine to refresh the token every 30 minutes coroutineScope.launch { while (true) { val refreshToken = getRefreshToken() if (refreshToken != null) { updateRefreshToken(refreshToken) } - - kotlinx.coroutines.delay(15 * 60 * 1000) // 15 minutes in milliseconds + log.info("Token refreshed, retrying in 15 minutes") + delay(15.minutes) } } } - private suspend fun refreshToken(refreshToken: String) = withContext(Dispatchers.IO) { - val client = OkHttpClient() - val url = URL(getControlPlaneUrl()).toURI().resolve("/auth/refresh").toURL() - val jsonBody = mapOf("refreshToken" to refreshToken) - val jsonString = Gson().toJson(jsonBody) - val requestBody = jsonString.toRequestBody("application/json".toMediaType()) - - val request = Request.Builder() - .url(url) - .post(requestBody) - .header("Content-Type", "application/json") - .build() - - val response = client.newCall(request).execute() - - val responseBody = response.body?.string() + private fun refreshToken(refreshToken: String): RefreshTokenResponse { val gson = Gson() - val responseMap = gson.fromJson(responseBody, Map::class.java) - - responseMap + val jsonBody = gson.toJson(mapOf("refreshToken" to refreshToken)) + val url = getControlPlaneUrl() + "/auth/refresh" + val response = HttpRequests.post(url, HttpRequests.JSON_CONTENT_TYPE) + .connect { connection -> connection.write(jsonBody.toByteArray()) } + .toString() + return gson.fromJson(response, RefreshTokenResponse::class.java) } - private fun openSignInPage(project: Project, useOnboarding: Boolean): String? { var authUrl: String? = null val coreMessenger = project.service().coreMessenger @@ -256,6 +229,17 @@ class ContinueAuthService { setAccountLabel(info.account.label) } + private data class RefreshTokenResponse( + val accessToken: String, + val refreshToken: String, + val user: User, + ) { + data class User( + val firstName: String, + val lastName: String, + val email: String + ) + } } // Data class to represent the ControlPlaneSessionInfo diff --git a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt index 4e41179851..057a875939 100644 --- a/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt +++ b/extensions/intellij/src/main/kotlin/com/github/continuedev/continueintellijextension/services/ContinueExtensionSettingsService.kt @@ -1,25 +1,25 @@ package com.github.continuedev.continueintellijextension.services -import com.github.continuedev.continueintellijextension.constants.getConfigJsPath import com.github.continuedev.continueintellijextension.constants.getConfigJsonPath -import com.intellij.execution.target.value.constant +import com.github.continuedev.continueintellijextension.error.ContinueErrorService import com.intellij.openapi.application.ApplicationInfo import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.* +import com.intellij.openapi.components.PersistentStateComponent +import com.intellij.openapi.components.State +import com.intellij.openapi.components.Storage +import com.intellij.openapi.components.service import com.intellij.openapi.options.Configurable import com.intellij.openapi.project.DumbAware -import com.intellij.openapi.util.SystemInfo import com.intellij.util.concurrency.AppExecutorUtil +import com.intellij.util.io.HttpRequests import com.intellij.util.messages.Topic -import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json -import okhttp3.OkHttpClient -import okhttp3.Request import java.awt.GridBagConstraints import java.awt.GridBagLayout import java.io.File import java.io.IOException +import java.net.URL import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit import javax.swing.* @@ -72,11 +72,10 @@ class ContinueSettingsComponent : DumbAware { } } -@Serializable -class ContinueRemoteConfigSyncResponse { - var configJson: String? = null - var configJs: String? = null -} +data class ContinueRemoteConfigSyncResponse( + var configJson: String?, + var configJs: String? +) @State( name = "com.github.continuedev.continueintellijextension.services.ContinueExtensionSettings", @@ -117,50 +116,23 @@ open class ContinueExtensionSettings : PersistentStateComponent - if (!response.isSuccessful) throw IOException("Unexpected code $response") - - response.body?.string()?.let { responseBody -> - try { - configResponse = - Json.decodeFromString(responseBody) - } catch (e: Exception) { - e.printStackTrace() - return - } - } - } + val url = "$baseUrl/sync" + val responseBody = HttpRequests.post(url, HttpRequests.JSON_CONTENT_TYPE) + .tuner { connection -> + if (token != null) + connection.addRequestProperty("Authorization", "Bearer $token") + }.readString() + val response = Json.decodeFromString(responseBody) + val file = File(getConfigJsonPath(URL(url).host)) + response.configJs.let { file.writeText(it!!) } + response.configJson.let { file.writeText(it!!) } } catch (e: IOException) { - e.printStackTrace() - return - } - - if (configResponse?.configJson?.isNotEmpty()!!) { - val file = File(getConfigJsonPath(request.url.host)) - file.writeText(configResponse!!.configJson!!) - } - - if (configResponse?.configJs?.isNotEmpty()!!) { - val file = File(getConfigJsPath(request.url.host)) - file.writeText(configResponse!!.configJs!!) + service().report(e, "Unexpected exception during remote config sync") } } } @@ -174,7 +146,7 @@ open class ContinueExtensionSettings : PersistentStateComponent