diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt index 190c27e04..501f5dc00 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/Auth.kt @@ -1,6 +1,7 @@ package io.github.jan.supabase.auth import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.SupabaseClientBuilder import io.github.jan.supabase.annotations.SupabaseExperimental import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.admin.AdminApi @@ -500,8 +501,13 @@ interface Auth : MainPlugin, CustomSerializationPlugin { const val API_VERSION = 1 override fun createConfig(init: AuthConfig.() -> Unit) = AuthConfig().apply(init) + override fun create(supabaseClient: SupabaseClient, config: AuthConfig): Auth = AuthImpl(supabaseClient, config) + override fun setup(builder: SupabaseClientBuilder, config: AuthConfig) { + + } + } } diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index a09eeaf45..e3830b738 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -13,7 +13,7 @@ import kotlin.time.Duration.Companion.seconds /** * The configuration for [Auth] */ -expect class AuthConfig() : CustomSerializationConfig, AuthConfigDefaults +expect class AuthConfig() : CustomSerializationConfig, AuthConfigDefaults, AuthDependentPluginConfig /** * The default values for the [AuthConfig] @@ -110,6 +110,16 @@ open class AuthConfigDefaults : MainConfig() { @SupabaseInternal var autoSetupPlatform: Boolean = true + /** + * Whether to check if the current session is expired on an authenticated request and possibly try to refresh it. + * + * **Note: This option is experimental and is a fail-safe for when the auto refresh fails. This option may be removed without notice.** + */ + @SupabaseExperimental + var checkSessionOnRequest: Boolean = true + + var requireValidSession: Boolean = false + } /** diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt new file mode 100644 index 000000000..3807edc86 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt @@ -0,0 +1,16 @@ +package io.github.jan.supabase.auth + +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.auth.user.UserSession + +/** + * TODO + */ +interface AuthDependentPluginConfig { + + /** + * Whether to require a valid [UserSession] in the [Auth] plugin to make any request with this plugin. The [SupabaseClient.supabaseKey] cannot be used as fallback. + */ + var requireValidSession: Boolean + +} diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt index 0511648fc..a99ebcd7e 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt @@ -87,8 +87,9 @@ internal class AuthImpl( override val codeVerifierCache = config.codeVerifierCache ?: createDefaultCodeVerifierCache() @OptIn(SupabaseInternal::class) - internal val api = supabaseClient.authenticatedSupabaseApi(this) - override val admin: AdminApi = AdminApiImpl(this) + internal val userApi = supabaseClient.authenticatedSupabaseApi(this) + internal val publicApi = supabaseClient.authenticatedSupabaseApi(this, requireSession = false) + override val admin: AdminApi = AdminApiImpl(publicApi) override val mfa: MfaApi = MfaApiImpl(this) var sessionJob: Job? = null override val isAutoRefreshRunning: Boolean @@ -148,7 +149,7 @@ internal class AuthImpl( }, redirectUrl, config) override suspend fun signInAnonymously(data: JsonObject?, captchaToken: String?) { - val response = api.postJson("signup", buildJsonObject { + val response = publicApi.postJson("signup", buildJsonObject { data?.let { put("data", it) } captchaToken?.let(::putCaptchaToken) }) @@ -172,7 +173,7 @@ internal class AuthImpl( val automaticallyOpen = ExternalAuthConfigDefaults().apply(config).automaticallyOpenUrl val fetchUrl: suspend (String?) -> String = { redirectTo: String? -> val url = getOAuthUrl(provider, redirectTo, "user/identities/authorize", config) - val response = api.rawRequest(url) { + val response = userApi.rawRequest(url) { method = HttpMethod.Get parameter("skip_http_redirect", true) } @@ -199,12 +200,12 @@ internal class AuthImpl( config: (IDToken.Config) -> Unit ) { val body = IDToken.Config(idToken = idToken, provider = provider, linkIdentity = true).apply(config) - val result = api.postJson("token?grant_type=id_token", body) + val result = userApi.postJson("token?grant_type=id_token", body) importSession(result.safeBody(), source = SessionSource.UserIdentitiesChanged(result.safeBody())) } override suspend fun unlinkIdentity(identityId: String, updateLocalUser: Boolean) { - api.delete("user/identities/$identityId") + userApi.delete("user/identities/$identityId") if (updateLocalUser) { val session = currentSessionOrNull() ?: return val newUser = session.user?.copy(identities = session.user.identities?.filter { it.identityId != identityId }) @@ -228,7 +229,7 @@ internal class AuthImpl( } val codeChallenge: String? = preparePKCEIfEnabled() - return api.postJson("sso", buildJsonObject { + return publicApi.postJson("sso", buildJsonObject { redirectUrl?.let { put("redirect_to", it) } createdConfig.captchaToken?.let(::putCaptchaToken) codeChallenge?.let(::putCodeChallenge) @@ -238,7 +239,8 @@ internal class AuthImpl( createdConfig.providerId?.let { put("provider_id", it) } - }).body() + }) + .body() } override suspend fun updateUser( @@ -252,7 +254,7 @@ internal class AuthImpl( putJsonObject(supabaseJson.encodeToJsonElement(updateBuilder).jsonObject) codeChallenge?.let(::putCodeChallenge) }.toString() - val response = api.putJson("user", body) { + val response = userApi.putJson("user", body) { redirectUrl?.let { url.parameters.append("redirect_to", it) } } val userInfo = response.safeBody() @@ -268,7 +270,7 @@ internal class AuthImpl( } private suspend fun resend(type: String, body: JsonObjectBuilder.() -> Unit) { - api.postJson("resend", buildJsonObject { + userApi.postJson("resend", buildJsonObject { put("type", type) putJsonObject(buildJsonObject(body)) }) @@ -303,19 +305,19 @@ internal class AuthImpl( captchaToken?.let(::putCaptchaToken) codeChallenge?.let(::putCodeChallenge) }.toString() - api.postJson("recover", body) { + publicApi.postJson("recover", body) { redirectUrl?.let { url.encodedParameters.append("redirect_to", it) } } } override suspend fun reauthenticate() { - api.get("reauthenticate") + userApi.get("reauthenticate") } override suspend fun signOut(scope: SignOutScope) { if (currentSessionOrNull() != null) { try { - api.post("logout") { + userApi.post("logout") { parameter("scope", scope.name.lowercase()) } } catch(e: RestException) { @@ -345,7 +347,7 @@ internal class AuthImpl( captchaToken?.let(::putCaptchaToken) additionalData() } - val response = api.postJson("verify", body) + val response = publicApi.postJson("verify", body) val session = response.body() importSession(session, source = SessionSource.SignIn(OTP)) } @@ -377,7 +379,7 @@ internal class AuthImpl( } override suspend fun retrieveUser(jwt: String): UserInfo { - val response = api.get("user") { + val response = userApi.get("user") { headers["Authorization"] = "Bearer $jwt" } val body = response.bodyAsText() @@ -400,7 +402,7 @@ internal class AuthImpl( require(codeVerifier != null) { "No code verifier stored. Make sure to use `getOAuthUrl` for the OAuth Url to prepare the PKCE flow." } - val session = api.postJson("token?grant_type=pkce", buildJsonObject { + val session = publicApi.postJson("token?grant_type=pkce", buildJsonObject { put("auth_code", code) put("code_verifier", codeVerifier) }) { @@ -420,7 +422,7 @@ internal class AuthImpl( val body = buildJsonObject { put("refresh_token", refreshToken) } - val response = api.postJson("token?grant_type=refresh_token", body) { + val response = publicApi.postJson("token?grant_type=refresh_token", body) { headers.remove("Authorization") } return response.safeBody("Auth#refreshSession") diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt index aca2fd113..7a685f3c5 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthenticatedSupabaseApi.kt @@ -1,31 +1,50 @@ @file:Suppress("UndocumentedPublicClass", "UndocumentedPublicFunction") package io.github.jan.supabase.auth +import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.exception.SessionRequiredException +import io.github.jan.supabase.auth.exception.TokenExpiredException import io.github.jan.supabase.exceptions.RestException +import io.github.jan.supabase.logging.e import io.github.jan.supabase.network.SupabaseApi +import io.github.jan.supabase.plugins.MainConfig import io.github.jan.supabase.plugins.MainPlugin import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.bearerAuth import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpStatement +import kotlin.time.Clock + +@SupabaseInternal +data class AuthenticatedApiConfig( + val jwtToken: String? = null, + val defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, + val requireSession: Boolean +) @OptIn(SupabaseInternal::class) class AuthenticatedSupabaseApi @SupabaseInternal constructor( resolveUrl: (path: String) -> String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, - private val defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, supabaseClient: SupabaseClient, - private val jwtToken: String? = null // Can be configured plugin-wide. By default, all plugins use the token from the current session + config: AuthenticatedApiConfig ): SupabaseApi(resolveUrl, parseErrorResponse, supabaseClient) { + private val defaultRequest = config.defaultRequest + private val jwtToken = config.jwtToken + private val requireSession = config.requireSession + override suspend fun rawRequest(url: String, builder: HttpRequestBuilder.() -> Unit): HttpResponse { - val accessToken = supabaseClient.resolveAccessToken(jwtToken) ?: error("No access token available") + val builder = HttpRequestBuilder().apply(builder) + val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException() + checkAccessToken(accessToken) return super.rawRequest(url) { bearerAuth(accessToken) - builder() defaultRequest?.invoke(this) + this } } @@ -35,14 +54,42 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( url: String, builder: HttpRequestBuilder.() -> Unit ): HttpStatement { + val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException() + checkAccessToken(accessToken) return super.prepareRequest(url) { - val jwtToken = jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey - bearerAuth(jwtToken) + bearerAuth(accessToken) builder() defaultRequest?.invoke(this) } } + private suspend fun checkAccessToken(token: String?) { + val currentSession = supabaseClient.auth.currentSessionOrNull() + val now = Clock.System.now() + val sessionExistsAndExpired = + token == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < now + val autoRefreshEnabled = supabaseClient.auth.config.alwaysAutoRefresh + if (sessionExistsAndExpired && autoRefreshEnabled) { + val autoRefreshRunning = supabaseClient.auth.isAutoRefreshRunning + Auth.logger.e { + """ + Authenticated request attempted with expired access token. This should not happen. Please report this issue. Trying to refresh session before... + Auto refresh running: $autoRefreshRunning + OS: ${OSInformation.CURRENT} + Session: $currentSession + """.trimIndent() + } + + try { + supabaseClient.auth.refreshCurrentSession() + } catch (e: Exception) { + Auth.logger.e(e) { "Failed to refresh session" } + throw TokenExpiredException() + } + } + } + } /** @@ -50,18 +97,33 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( * All requests will be resolved relative to this url */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(baseUrl: String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null) = authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse) +fun SupabaseClient.authenticatedSupabaseApi( + baseUrl: String, + parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, + config: AuthenticatedApiConfig +) = + authenticatedSupabaseApi({ baseUrl + it }, parseErrorResponse, config) /** * Creates a [AuthenticatedSupabaseApi] for the given [plugin]. Requires [Auth] to authenticate requests * All requests will be resolved using the [MainPlugin.resolveUrl] function */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(plugin: MainPlugin<*>, defaultRequest: (HttpRequestBuilder.() -> Unit)? = null) = authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, defaultRequest, plugin.config.jwtToken) +fun SupabaseClient.authenticatedSupabaseApi( + plugin: MainPlugin, + defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, + requireSession: Boolean = plugin.config.requireValidSession +): AuthenticatedSupabaseApi where C : MainConfig, C : AuthDependentPluginConfig = + authenticatedSupabaseApi(plugin::resolveUrl, plugin::parseErrorResponse, AuthenticatedApiConfig(defaultRequest = defaultRequest, requireSession = requireSession)) /** * Creates a [AuthenticatedSupabaseApi] with the given [resolveUrl] function. Requires [Auth] to authenticate requests * All requests will be resolved using this function */ @SupabaseInternal -fun SupabaseClient.authenticatedSupabaseApi(resolveUrl: (path: String) -> String, parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, defaultRequest: (HttpRequestBuilder.() -> Unit)? = null, jwtToken: String? = null) = AuthenticatedSupabaseApi(resolveUrl, parseErrorResponse, defaultRequest, this, jwtToken) \ No newline at end of file +fun SupabaseClient.authenticatedSupabaseApi( + resolveUrl: (path: String) -> String, + parseErrorResponse: (suspend (response: HttpResponse) -> RestException)? = null, + config: AuthenticatedApiConfig +) = + AuthenticatedSupabaseApi(resolveUrl, parseErrorResponse, this, config) \ No newline at end of file diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt index bca33394c..546a1173d 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/admin/AdminApi.kt @@ -2,7 +2,7 @@ package io.github.jan.supabase.auth.admin import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.auth.Auth -import io.github.jan.supabase.auth.AuthImpl +import io.github.jan.supabase.auth.AuthenticatedSupabaseApi import io.github.jan.supabase.auth.SignOutScope import io.github.jan.supabase.auth.user.UserInfo import io.github.jan.supabase.auth.user.UserMfaFactor @@ -99,9 +99,7 @@ interface AdminApi { } @PublishedApi -internal class AdminApiImpl(val gotrue: Auth) : AdminApi { - - val api = (gotrue as AuthImpl).api +internal class AdminApiImpl(val api: AuthenticatedSupabaseApi) : AdminApi { override suspend fun signOut(jwt: String, scope: SignOutScope) { api.post("logout") { diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt new file mode 100644 index 000000000..4529dbc9c --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt @@ -0,0 +1,6 @@ +package io.github.jan.supabase.auth.exception + +/** + * An exception thrown when trying to perform a request that requires a valid session while no user is logged in. + */ +class SessionRequiredException: Exception("You need to be logged in to perform this request") \ No newline at end of file diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt new file mode 100644 index 000000000..2e6b99564 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt @@ -0,0 +1,4 @@ +package io.github.jan.supabase.auth.exception + +//TODO: Add actual message and docs +class TokenExpiredException: Exception("The token has expired") \ No newline at end of file diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt index fe9259a4a..a89bc931d 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/mfa/MfaApi.kt @@ -128,7 +128,7 @@ internal class MfaApiImpl( override val verifiedFactors: List get() = auth.currentUserOrNull()?.factors?.filter(UserMfaFactor::isVerified) ?: emptyList() - val api = auth.api + val api = auth.userApi override suspend fun enroll(factorType: FactorType, friendlyName: String?, config: Config.() -> Unit): MfaFactor { val result = api.postJson("factors", buildJsonObject { diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt index 3cfcfa2b0..13fce1827 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/DefaultAuthProvider.kt @@ -59,7 +59,7 @@ sealed interface DefaultAuthProvider : AuthProvider { val encodedCredentials = encodeCredentials(config) val gotrue = supabaseClient.auth as AuthImpl val url = "token?grant_type=$grantType" - val response = gotrue.api.postJson(url, encodedCredentials) { + val response = gotrue.publicApi.postJson(url, encodedCredentials) { redirectUrl?.let { redirectTo(it) } } response.body().also { @@ -87,7 +87,7 @@ sealed interface DefaultAuthProvider : AuthProvider { Phone -> "signup" IDToken -> "token?grant_type=id_token" } - val response = gotrue.api.postJson(url, buildJsonObject { + val response = gotrue.publicApi.postJson(url, buildJsonObject { putJsonObject(body) if (codeChallenge != null) { putCodeChallenge(codeChallenge) diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt index 3875f0d62..b1d6e1b86 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/providers/builtin/OTP.kt @@ -82,7 +82,7 @@ data object OTP: AuthProvider { supabaseClient.auth.codeVerifierCache.saveCodeVerifier(codeVerifier) codeChallenge = generateCodeChallenge(codeVerifier) } - (supabaseClient.auth as AuthImpl).api.postJson("otp", buildJsonObject { + (supabaseClient.auth as AuthImpl).publicApi.postJson("otp", buildJsonObject { putJsonObject(body) codeChallenge?.let { put("code_challenge", it) diff --git a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt index 960ef4e75..8f60628a7 100644 --- a/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt +++ b/Postgrest/src/commonMain/kotlin/io/github/jan/supabase/postgrest/Postgrest.kt @@ -2,6 +2,7 @@ package io.github.jan.supabase.postgrest import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseSerializer +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.exceptions.HttpRequestException import io.github.jan.supabase.logging.SupabaseLogger import io.github.jan.supabase.plugins.CustomSerializationConfig @@ -101,7 +102,8 @@ interface Postgrest : MainPlugin, CustomSerializationPlugin { data class Config( var defaultSchema: String = "public", var propertyConversionMethod: PropertyConversionMethod = PropertyConversionMethod.CAMEL_CASE_TO_SNAKE_CASE, - ): MainConfig(), CustomSerializationConfig { + override var requireValidSession: Boolean = false, + ): MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { override var serializer: SupabaseSerializer? = null diff --git a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt index cb2a0cb50..eb13755c1 100644 --- a/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt +++ b/Realtime/src/commonMain/kotlin/io/github/jan/supabase/realtime/Realtime.kt @@ -4,6 +4,7 @@ import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseClientBuilder import io.github.jan.supabase.SupabaseSerializer import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.auth.resolveAccessToken import io.github.jan.supabase.logging.SupabaseLogger import io.github.jan.supabase.logging.w @@ -141,7 +142,8 @@ interface Realtime : MainPlugin, CustomSerializationPlugin { var connectOnSubscribe: Boolean = true, @property:SupabaseInternal var websocketFactory: RealtimeWebsocketFactory? = null, var disconnectOnNoSubscriptions: Boolean = true, - ): MainConfig(), CustomSerializationConfig { + override var requireValidSession: Boolean = false, + ): MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { /** * A custom access token provider. If this is set, the [SupabaseClient] will not be used to resolve the access token. diff --git a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt index 7885c8225..8893e8176 100644 --- a/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt +++ b/Storage/src/commonMain/kotlin/io/github/jan/supabase/storage/Storage.kt @@ -3,6 +3,7 @@ package io.github.jan.supabase.storage import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseSerializer import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.auth.authenticatedSupabaseApi import io.github.jan.supabase.bodyOrNull import io.github.jan.supabase.collections.AtomicMutableMap @@ -138,8 +139,9 @@ interface Storage : MainPlugin, CustomSerializationPlugin { data class Config( var transferTimeout: Duration = 120.seconds, @PublishedApi internal var resumable: Resumable = Resumable(), - override var serializer: SupabaseSerializer? = null - ) : MainConfig(), CustomSerializationConfig { + override var serializer: SupabaseSerializer? = null, + override var requireValidSession: Boolean = false, + ) : MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { /** * @param cache the cache for caching resumable upload urls diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt index 061c6c156..c97d1e6bb 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClient.kt @@ -18,6 +18,11 @@ import kotlinx.coroutines.CoroutineDispatcher */ interface SupabaseClient { + /** + * The configuration for the Supabase Client. + */ + val config: SupabaseClientConfig + /** * The supabase url with either a http or https scheme. */ @@ -93,7 +98,7 @@ interface SupabaseClient { } internal class SupabaseClientImpl( - config: SupabaseClientConfig, + override val config: SupabaseClientConfig, ) : SupabaseClient { override val accessToken: AccessTokenProvider? = config.accessToken @@ -117,11 +122,7 @@ internal class SupabaseClientImpl( @OptIn(SupabaseInternal::class) override val httpClient = KtorSupabaseHttpClient( - supabaseKey, - config.networkConfig.httpConfigOverrides, - config.networkConfig.requestTimeout.inWholeMilliseconds, - config.networkConfig.httpEngine, - config.osInformation + this ) override val pluginManager = PluginManager(config.plugins.toList().associate { (key, value) -> diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt index fa2efedd1..05e8194f9 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientBuilder.kt @@ -94,7 +94,6 @@ class SupabaseClientBuilder @PublishedApi internal constructor(private val supab * The current operating system information. */ var osInformation: OSInformation? = OSInformation.CURRENT - private val httpConfigOverrides = mutableListOf() private val plugins = mutableMapOf() diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt index 3e6983ef1..f1e039f77 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/SupabaseClientConfig.kt @@ -5,7 +5,7 @@ import io.ktor.client.engine.HttpClientEngine import kotlinx.coroutines.CoroutineDispatcher import kotlin.time.Duration -internal data class SupabaseClientConfig( +data class SupabaseClientConfig( val supabaseUrl: String, val supabaseKey: String, val defaultLogLevel: LogLevel, @@ -17,7 +17,7 @@ internal data class SupabaseClientConfig( val osInformation: OSInformation? ) -internal data class SupabaseNetworkConfig( +data class SupabaseNetworkConfig( val useHTTPS: Boolean, val httpEngine: HttpClientEngine?, val httpConfigOverrides: List, diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt index c3e01deba..ab606e7a1 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/KtorSupabaseHttpClient.kt @@ -2,7 +2,6 @@ package io.github.jan.supabase.network import io.github.jan.supabase.BuildConfig -import io.github.jan.supabase.OSInformation import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal import io.github.jan.supabase.exceptions.HttpRequestException @@ -11,7 +10,6 @@ import io.github.jan.supabase.logging.e import io.github.jan.supabase.supabaseJson import io.ktor.client.HttpClient import io.ktor.client.HttpClientConfig -import io.ktor.client.engine.HttpClientEngine import io.ktor.client.plugins.DefaultRequest import io.ktor.client.plugins.HttpRequestTimeoutException import io.ktor.client.plugins.HttpTimeout @@ -40,15 +38,19 @@ typealias HttpRequestOverride = HttpRequestBuilder.() -> Unit */ @OptIn(SupabaseInternal::class) class KtorSupabaseHttpClient @SupabaseInternal constructor( - private val supabaseKey: String, - modifiers: List.() -> Unit> = listOf(), - private val requestTimeout: Long, - engine: HttpClientEngine? = null, - private val osInformation: OSInformation? + private val supabase: SupabaseClient ): SupabaseHttpClient() { + private val supabaseKey = supabase.supabaseKey + private val osInformation = supabase.config.osInformation + + private val networkConfig = supabase.config.networkConfig + private val requestTimeout = networkConfig.requestTimeout + private val engine = networkConfig.httpEngine + private val modifiers = networkConfig.httpConfigOverrides + init { - SupabaseClient.LOGGER.d { "Creating KtorSupabaseHttpClient with request timeout $requestTimeout ms, HttpClientEngine: $engine" } + SupabaseClient.LOGGER.d { "Creating KtorSupabaseHttpClient with request timeout $requestTimeout, HttpClientEngine: $engine" } } @SupabaseInternal @@ -63,11 +65,10 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( } val endPoint = request.url.encodedPath SupabaseClient.LOGGER.d { "Starting ${request.method.value} request to endpoint $endPoint" } - val response = try { httpClient.request(url, builder) } catch(e: HttpRequestTimeoutException) { - SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint timed out after $requestTimeout ms" } + SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint timed out after $requestTimeout" } throw e } catch(e: CancellationException) { SupabaseClient.LOGGER.e { "${request.method.value} request to endpoint $endPoint was cancelled"} @@ -92,7 +93,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( val response = try { httpClient.prepareRequest(url, builder) } catch(e: HttpRequestTimeoutException) { - SupabaseClient.LOGGER.e { "Request timed out after $requestTimeout ms on url $url" } + SupabaseClient.LOGGER.e { "Request timed out after $requestTimeout on url $url" } throw e } catch(e: CancellationException) { SupabaseClient.LOGGER.e { "Request was cancelled on url $url" } @@ -127,7 +128,7 @@ class KtorSupabaseHttpClient @SupabaseInternal constructor( json(supabaseJson) } install(HttpTimeout) { - requestTimeoutMillis = requestTimeout + requestTimeoutMillis = requestTimeout.inWholeMilliseconds } modifiers.forEach { it.invoke(this) } } diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt new file mode 100644 index 000000000..0ef5ee258 --- /dev/null +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/network/NetworkInterceptor.kt @@ -0,0 +1,23 @@ +package io.github.jan.supabase.network + +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.annotations.SupabaseInternal +import io.ktor.client.request.HttpRequestBuilder +import io.ktor.client.statement.HttpResponse + +@SupabaseInternal +sealed interface NetworkInterceptor { + + fun interface Before: NetworkInterceptor { + + suspend fun call(builder: HttpRequestBuilder, supabase: SupabaseClient) + + } + + fun interface After: NetworkInterceptor { + + suspend fun call(response: HttpResponse, supabase: SupabaseClient) + + } + +} \ No newline at end of file