diff --git a/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index e20e9fec3..f262c0e15 100644 --- a/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/androidMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -2,12 +2,11 @@ package io.github.jan.supabase.auth import androidx.browser.customtabs.CustomTabsIntent import io.github.jan.supabase.auth.providers.ExternalAuthConfig -import io.github.jan.supabase.plugins.CustomSerializationConfig /** * The configuration for [Auth] */ -actual class AuthConfig : CustomSerializationConfig, AuthConfigDefaults() { +actual class AuthConfig : AuthConfigDefaults() { /** * The action to use for the OAuth flow. Can be overriden per-request in the [ExternalAuthConfig] diff --git a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AccessToken.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AccessToken.kt index ed2a5e6ce..382262d2b 100644 --- a/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AccessToken.kt +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AccessToken.kt @@ -1,15 +1,20 @@ package io.github.jan.supabase.auth +import io.github.jan.supabase.OSInformation +import io.github.jan.supabase.StringMasking import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.exception.TokenExpiredException +import io.github.jan.supabase.logging.e import io.github.jan.supabase.plugins.MainConfig import io.github.jan.supabase.plugins.MainPlugin +import kotlin.time.Clock /** * Returns the access token used for requests. The token is resolved in the following order: * 1. [jwtToken] if not null - * 2. [SupabaseClient.resolveAccessToken] if not null - * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed + * 2. [SupabaseClient.accessToken] if not null + * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed. This method also checks if the token expired and tries to force-refresh it. * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true */ @SupabaseInternal @@ -19,18 +24,45 @@ suspend fun SupabaseClient.resolveAccessToken( ): String? { val key = if(keyAsFallback) supabaseKey else null return jwtToken ?: accessToken?.invoke() - ?: pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: key + ?: pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull()?.also { checkAccessToken(it) } ?: key } /** * Returns the access token used for requests. The token is resolved in the following order: * 1. [MainConfig.jwtToken] if not null * 2. [SupabaseClient.resolveAccessToken] if not null - * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed + * 3. [Auth.currentAccessTokenOrNull] if the Auth plugin is installed. This method also checks if the token expired and tries to force-refresh it. * 4. [SupabaseClient.supabaseKey] if [keyAsFallback] is true */ @SupabaseInternal suspend fun SupabaseClient.resolveAccessToken( plugin: MainPlugin, keyAsFallback: Boolean = true -) = resolveAccessToken(plugin.config.jwtToken, keyAsFallback) \ No newline at end of file +) = resolveAccessToken(plugin.config.jwtToken, keyAsFallback) + +private suspend fun SupabaseClient.checkAccessToken(token: String) { + val auth = pluginManager.getPluginOrNull(Auth) ?: return + val currentSession = auth.currentSessionOrNull() + val now = Clock.System.now() + val sessionExistsAndExpired = + token == currentSession?.accessToken && currentSession != null && currentSession.expiresAt < now + val autoRefreshEnabled = auth.config.alwaysAutoRefresh + if (sessionExistsAndExpired && autoRefreshEnabled) { + val autoRefreshRunning = 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: ${StringMasking.maskSession(currentSession)} + """.trimIndent() + } + + try { + auth.refreshCurrentSession() + } catch (e: Exception) { + Auth.logger.e(e) { "Failed to force-refresh session before making a request with an expired access token" } + throw TokenExpiredException() + } + } +} \ No newline at end of file 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..9b18ba308 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 @@ -500,6 +500,7 @@ 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) } 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..a1accb43b 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 @@ -1,9 +1,11 @@ package io.github.jan.supabase.auth +import io.github.jan.supabase.SupabaseClient import io.github.jan.supabase.SupabaseClientBuilder import io.github.jan.supabase.SupabaseSerializer import io.github.jan.supabase.annotations.SupabaseExperimental import io.github.jan.supabase.annotations.SupabaseInternal +import io.github.jan.supabase.auth.user.UserSession import io.github.jan.supabase.plugins.CustomSerializationConfig import io.github.jan.supabase.plugins.MainConfig import kotlinx.coroutines.CoroutineDispatcher @@ -13,12 +15,12 @@ import kotlin.time.Duration.Companion.seconds /** * The configuration for [Auth] */ -expect class AuthConfig() : CustomSerializationConfig, AuthConfigDefaults +expect class AuthConfig() : AuthConfigDefaults /** * The default values for the [AuthConfig] */ -open class AuthConfigDefaults : MainConfig() { +open class AuthConfigDefaults : MainConfig(), AuthDependentPluginConfig, CustomSerializationConfig { /** * The duration after which [Auth] should retry refreshing a session, when it failed due to network issues @@ -64,7 +66,7 @@ open class AuthConfigDefaults : MainConfig() { /** * A serializer used for serializing/deserializing objects e.g. in [Auth.signInWith]. Defaults to [SupabaseClientBuilder.defaultSerializer], when null. */ - var serializer: SupabaseSerializer? = null + override var serializer: SupabaseSerializer? = null /** * The deeplink scheme used for the implicit and PKCE flow. When null, deeplinks won't be used as redirect urls @@ -110,6 +112,20 @@ 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 + + /** + * 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. + */ + @SupabaseExperimental + override 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..e0504e0e8 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthDependentPluginConfig.kt @@ -0,0 +1,18 @@ +package io.github.jan.supabase.auth + +import io.github.jan.supabase.SupabaseClient +import io.github.jan.supabase.annotations.SupabaseExperimental +import io.github.jan.supabase.auth.user.UserSession + +/** + * Configuration for plugins depending on the Auth plugin + */ +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. + */ + @SupabaseExperimental + 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..74eab973c 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 @@ -86,9 +86,10 @@ internal class AuthImpl( override val sessionManager = config.sessionManager ?: createDefaultSessionManager() override val codeVerifierCache = config.codeVerifierCache ?: createDefaultCodeVerifierCache() + internal val publicApi = supabaseClient.authenticatedSupabaseApi(this, requireSession = false) @OptIn(SupabaseInternal::class) - internal val api = supabaseClient.authenticatedSupabaseApi(this) - override val admin: AdminApi = AdminApiImpl(this) + internal val userApi = if(config.requireValidSession) supabaseClient.authenticatedSupabaseApi(this) else publicApi + 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/AuthStringMasking.kt b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthStringMasking.kt new file mode 100644 index 000000000..7a10e6aac --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthStringMasking.kt @@ -0,0 +1,13 @@ +package io.github.jan.supabase.auth + +import io.github.jan.supabase.StringMasking +import io.github.jan.supabase.auth.user.UserSession + +internal fun StringMasking.maskSession(value: UserSession): UserSession { + return value.copy( + accessToken = maskString(value.accessToken, showLength = true), + refreshToken = maskString(value.refreshToken, showLength = true), + providerRefreshToken = value.providerRefreshToken?.let { maskString(it, showLength = true) }, + providerToken = value.providerToken?.let { maskString(it, showLength = true) } + ) +} \ No newline at end of file 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..a3dd82249 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 @@ -3,29 +3,42 @@ package io.github.jan.supabase.auth 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.exceptions.RestException 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 +@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 accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException(url) return super.rawRequest(url) { bearerAuth(accessToken) - builder() defaultRequest?.invoke(this) + builder() } } @@ -35,9 +48,10 @@ class AuthenticatedSupabaseApi @SupabaseInternal constructor( url: String, builder: HttpRequestBuilder.() -> Unit ): HttpStatement { + val accessToken = supabaseClient.resolveAccessToken(jwtToken, keyAsFallback = !requireSession) + ?: throw SessionRequiredException(url) return super.prepareRequest(url) { - val jwtToken = jwtToken ?: supabaseClient.pluginManager.getPluginOrNull(Auth)?.currentAccessTokenOrNull() ?: supabaseClient.supabaseKey - bearerAuth(jwtToken) + bearerAuth(accessToken) builder() defaultRequest?.invoke(this) } @@ -50,18 +64,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, jwtToken = plugin.config.jwtToken)) /** * 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..b5f6d026c --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/SessionRequiredException.kt @@ -0,0 +1,12 @@ +package io.github.jan.supabase.auth.exception + +import io.github.jan.supabase.StringMasking +import io.ktor.http.Url + +/** + * An exception thrown when trying to perform a request that requires a valid session while no user is logged in. + * @param url The request url + */ +class SessionRequiredException(url: String): Exception("You need to be logged in to perform this request\nURL: ${StringMasking.maskUrl( + Url(url) +)}") 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..fea112d18 --- /dev/null +++ b/Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/exception/TokenExpiredException.kt @@ -0,0 +1,6 @@ +package io.github.jan.supabase.auth.exception + +/** + * Exception thrown when trying to make a request with an expired access token and the force-refresh failed. + */ +class TokenExpiredException: Exception("The token has expired and a force-refresh was unsuccessful") \ 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..fc59210f7 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 @@ -121,14 +121,14 @@ internal class MfaApiImpl( active = current == AuthenticatorAssuranceLevel.AAL2 ) } else { - MfaStatus(false, false) + MfaStatus(enabled = false, active = false) } } 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/Auth/src/commonTest/kotlin/AuthenticatedSupabaseApiTest.kt b/Auth/src/commonTest/kotlin/AuthenticatedSupabaseApiTest.kt new file mode 100644 index 000000000..d991909b1 --- /dev/null +++ b/Auth/src/commonTest/kotlin/AuthenticatedSupabaseApiTest.kt @@ -0,0 +1,272 @@ +import io.github.jan.supabase.SupabaseClientBuilder +import io.github.jan.supabase.auth.Auth +import io.github.jan.supabase.auth.AuthenticatedApiConfig +import io.github.jan.supabase.auth.auth +import io.github.jan.supabase.auth.authenticatedSupabaseApi +import io.github.jan.supabase.auth.exception.SessionRequiredException +import io.github.jan.supabase.auth.minimalConfig +import io.github.jan.supabase.auth.user.UserSession +import io.github.jan.supabase.testing.createMockedSupabaseClient +import io.ktor.client.engine.mock.respond +import io.ktor.client.request.header +import io.ktor.http.HttpMethod +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +class AuthenticatedSupabaseApiTest { + + private val configuration: SupabaseClientBuilder.() -> Unit = { + install(Auth) { + minimalConfig() + } + } + + @Test + fun testRequestWithBearerToken() = runTest { + val expectedToken = "test-access-token" + val expectedUrl = "test.url.de/" + + val client = createMockedSupabaseClient(configuration = configuration) { + assertEquals("Bearer $expectedToken", it.headers["Authorization"]) + respond("") + } + + client.auth.importSession(createSession(expectedToken)) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { expectedUrl + it }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + api.get("test") + } + + @Test + fun testRequestWithCustomHeaders() = runTest { + val expectedToken = "test-access-token" + val expectedHeaderName = "X-Custom-Header" + val expectedHeaderValue = "custom-value" + + val client = createMockedSupabaseClient(configuration = configuration) { + assertEquals("Bearer $expectedToken", it.headers["Authorization"]) + assertEquals(expectedHeaderValue, it.headers[expectedHeaderName]) + respond("") + } + + client.auth.importSession(createSession(expectedToken)) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = { + header(expectedHeaderName, expectedHeaderValue) + }, + requireSession = true + ) + ) + + api.get("test") + } + + @Test + fun testRequestWithCustomJwtToken() = runTest { + val customToken = "custom-jwt-token" + + val client = createMockedSupabaseClient(configuration = configuration) { + assertEquals("Bearer $customToken", it.headers["Authorization"]) + respond("") + } + + client.auth.importSession(createSession("different-token")) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = customToken, + defaultRequest = null, + requireSession = true + ) + ) + + api.get("test") + } + + @Test + fun testRequestWithoutSessionThrowsException() = runTest { + val client = createMockedSupabaseClient(configuration = configuration) { + respond("") + } + + client.auth.awaitInitialization() + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + assertFailsWith { + api.get("test") + } + } + + @Test + fun testRequestWithoutSessionButNotRequiredUsesApiKey() = runTest { + val apiKey = "test-api-key" + + val client = createMockedSupabaseClient( + supabaseKey = apiKey, + configuration = configuration + ) { + assertEquals("Bearer $apiKey", it.headers["Authorization"]) + respond("") + } + + client.auth.awaitInitialization() + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = false + ) + ) + + api.get("test") + } + + @Test + fun testUrlResolution() = runTest { + val baseUrl = "https://api.example.com" + val path = "endpoint/test" + var requestUrl = "" + + val client = createMockedSupabaseClient(configuration = configuration) { + requestUrl = it.url.toString() + respond("") + } + + client.auth.importSession(createSession("test-token")) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "$baseUrl/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + api.get(path) + + assertTrue(requestUrl.contains(baseUrl)) + assertTrue(requestUrl.contains(path)) + } + + @Test + fun testRawRequestWithoutPath() = runTest { + val baseUrl = "https://api.example.com" + var requestUrl = "" + + val client = createMockedSupabaseClient(configuration = configuration) { + requestUrl = it.url.toString() + respond("") + } + + client.auth.importSession(createSession("test-token")) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "$baseUrl/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + api.get("") + + assertTrue(requestUrl.contains(baseUrl)) + } + + @Test + fun testPrepareRequest() = runTest { + val expectedToken = "test-access-token" + + val client = createMockedSupabaseClient(configuration = configuration) { + assertEquals("Bearer $expectedToken", it.headers["Authorization"]) + assertEquals(HttpMethod.Post, it.method) + respond("") + } + + client.auth.importSession(createSession(expectedToken)) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + val statement = api.prepareRequest("test") { + method = HttpMethod.Post + } + statement.execute() + } + + @Test + fun testPrepareRequestWithExpiredSession() = runTest { + val client = createMockedSupabaseClient(configuration = { + install(Auth) { + minimalConfig() + alwaysAutoRefresh = false + } + }) { + respond("") + } + + client.auth.awaitInitialization() + client.auth.importSession(createSession("expired-token", expiresIn = 0)) + + val api = client.authenticatedSupabaseApi( + resolveUrl = { "test.url.de/$it" }, + config = AuthenticatedApiConfig( + jwtToken = null, + defaultRequest = null, + requireSession = true + ) + ) + + val statement = api.prepareRequest("test") { + method = HttpMethod.Get + } + statement.execute() + } + + private fun createSession( + accessToken: String, + expiresIn: Long = 3600, + refreshToken: String = "refresh-token" + ) = UserSession( + accessToken = accessToken, + refreshToken = refreshToken, + expiresIn = expiresIn, + tokenType = "Bearer", + user = null + ) + +} \ No newline at end of file diff --git a/Auth/src/desktopMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/desktopMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index 5304677f1..8e1a5f38f 100644 --- a/Auth/src/desktopMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/desktopMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -1,14 +1,13 @@ package io.github.jan.supabase.auth import io.github.jan.supabase.auth.server.HttpCallbackHtml -import io.github.jan.supabase.plugins.CustomSerializationConfig import kotlin.time.Duration import kotlin.time.Duration.Companion.minutes /** * The configuration for [Auth] */ -actual class AuthConfig : CustomSerializationConfig, AuthConfigDefaults() { +actual class AuthConfig : AuthConfigDefaults() { internal var httpCallbackConfig: HttpCallbackConfig = HttpCallbackConfig() diff --git a/Auth/src/iosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/iosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index b7229ecd9..d41131ace 100644 --- a/Auth/src/iosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/iosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -1,8 +1,6 @@ package io.github.jan.supabase.auth -import io.github.jan.supabase.plugins.CustomSerializationConfig - /** * The configuration for [Auth] */ -actual class AuthConfig : CustomSerializationConfig, AuthConfigDefaults() \ No newline at end of file +actual class AuthConfig : AuthConfigDefaults() \ No newline at end of file diff --git a/Auth/src/tvosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/tvosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index b7229ecd9..d41131ace 100644 --- a/Auth/src/tvosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/tvosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -1,8 +1,6 @@ package io.github.jan.supabase.auth -import io.github.jan.supabase.plugins.CustomSerializationConfig - /** * The configuration for [Auth] */ -actual class AuthConfig : CustomSerializationConfig, AuthConfigDefaults() \ No newline at end of file +actual class AuthConfig : AuthConfigDefaults() \ No newline at end of file diff --git a/Auth/src/watchosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/watchosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index b7229ecd9..d41131ace 100644 --- a/Auth/src/watchosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/watchosMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -1,8 +1,6 @@ package io.github.jan.supabase.auth -import io.github.jan.supabase.plugins.CustomSerializationConfig - /** * The configuration for [Auth] */ -actual class AuthConfig : CustomSerializationConfig, AuthConfigDefaults() \ No newline at end of file +actual class AuthConfig : AuthConfigDefaults() \ No newline at end of file diff --git a/Auth/src/webMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt b/Auth/src/webMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt index 1b6d6293d..d73a95356 100644 --- a/Auth/src/webMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt +++ b/Auth/src/webMain/kotlin/io/github/jan/supabase/auth/AuthConfig.kt @@ -1,13 +1,12 @@ package io.github.jan.supabase.auth import io.github.jan.supabase.annotations.SupabaseInternal -import io.github.jan.supabase.plugins.CustomSerializationConfig import io.ktor.util.PlatformUtils.IS_BROWSER /** * The configuration for [Auth] */ -actual class AuthConfig: CustomSerializationConfig, AuthConfigDefaults() { +actual class AuthConfig: AuthConfigDefaults() { /** * Whether to disable automatic URL checking for PKCE codes, error codes, and session tokens. diff --git a/Functions/src/commonMain/kotlin/io/github/jan/supabase/functions/Functions.kt b/Functions/src/commonMain/kotlin/io/github/jan/supabase/functions/Functions.kt index 8d74ebf17..3158998c6 100644 --- a/Functions/src/commonMain/kotlin/io/github/jan/supabase/functions/Functions.kt +++ b/Functions/src/commonMain/kotlin/io/github/jan/supabase/functions/Functions.kt @@ -4,6 +4,7 @@ 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.Auth +import io.github.jan.supabase.auth.AuthDependentPluginConfig import io.github.jan.supabase.auth.authenticatedSupabaseApi import io.github.jan.supabase.encode import io.github.jan.supabase.exceptions.BadRequestRestException @@ -141,7 +142,7 @@ class Functions(override val config: Config, override val supabaseClient: Supaba * @param jwtToken A jwt token to use for the requests. If not provided, the token from the [Auth] plugin, or the supabaseKey will be used * @property serializer A serializer used for serializing/deserializing objects e.g. in [Functions.invoke] or [EdgeFunction.invoke]. Defaults to [KotlinXSerializer] */ - class Config : MainConfig(), CustomSerializationConfig { + class Config : MainConfig(), CustomSerializationConfig, AuthDependentPluginConfig { override var serializer: SupabaseSerializer? = null @@ -150,6 +151,8 @@ class Functions(override val config: Config, override val supabaseClient: Supaba */ var defaultRegion: FunctionRegion = FunctionRegion.ANY + override var requireValidSession: Boolean = false + } /** 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..8caee6b97 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 @@ -210,11 +212,11 @@ internal class StorageImpl(override val supabaseClient: SupabaseClient, override override val serializer: SupabaseSerializer = config.serializer ?: supabaseClient.defaultSerializer @OptIn(SupabaseInternal::class) - internal val api = supabaseClient.authenticatedSupabaseApi(this) { + internal val api = supabaseClient.authenticatedSupabaseApi(this, defaultRequest = { timeout { requestTimeoutMillis = config.transferTimeout.inWholeMilliseconds } - } + }) private val resumableClients = AtomicMutableMap() diff --git a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/StringMasking.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/StringMasking.kt index c45ed3338..48c75381d 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/StringMasking.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/StringMasking.kt @@ -1,26 +1,30 @@ package io.github.jan.supabase +import io.github.jan.supabase.annotations.SupabaseInternal import io.ktor.http.Headers import io.ktor.http.Url import io.ktor.util.toMap -internal fun maskString(value: String, visibleCharacters: Int = 2, showLength: Boolean = false): String { - if(value.isBlank()) return value; - return value.take(visibleCharacters) + "..." + if(showLength) " (len=${value.length})" else "" -} +@SupabaseInternal +object StringMasking { + fun maskString(value: String, visibleCharacters: Int = 2, showLength: Boolean = false): String { + if(value.isBlank()) return value; + return value.take(visibleCharacters) + "..." + if(showLength) " (len=${value.length})" else "" + } -internal fun maskUrl(value: Url, visibleCharacters: Int = 2): String { - return buildUrl(value) { - host = "${host.take(visibleCharacters)}..." + fun maskUrl(value: Url, visibleCharacters: Int = 2): String { + return buildUrl(value) { + host = "${host.take(visibleCharacters)}..." + } } -} -private val SENSITIVE_HEADERS = listOf("apikey", "Authorization") + private val SENSITIVE_HEADERS = listOf("apikey", "Authorization") -internal fun maskHeaders(headers: Headers): String = headers.toMap().mapValues { (key, value) -> - if(key in SENSITIVE_HEADERS) { - value.firstOrNull()?.let { - listOf(if(key == "Authorization") "Bearer ${maskString(it.drop(7), showLength = true)}" else maskString(it, showLength = true)) - } - } else value -}.toString() + fun maskHeaders(headers: Headers): String = headers.toMap().mapValues { (key, value) -> + if(key in SENSITIVE_HEADERS) { + value.firstOrNull()?.let { + listOf(if(key == "Authorization") "Bearer ${maskString(it.drop(7), showLength = true)}" else maskString(it, showLength = true)) + } + } else value + }.toString() +} 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/exceptions/RestException.kt b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/exceptions/RestException.kt index 5194bf7a7..981b31fd2 100644 --- a/Supabase/src/commonMain/kotlin/io/github/jan/supabase/exceptions/RestException.kt +++ b/Supabase/src/commonMain/kotlin/io/github/jan/supabase/exceptions/RestException.kt @@ -1,7 +1,7 @@ package io.github.jan.supabase.exceptions -import io.github.jan.supabase.maskHeaders -import io.github.jan.supabase.maskUrl +import io.github.jan.supabase.StringMasking.maskHeaders +import io.github.jan.supabase.StringMasking.maskUrl import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.request 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/commonTest/kotlin/StringMaskingTest.kt b/Supabase/src/commonTest/kotlin/StringMaskingTest.kt index 5826ca389..b46388a21 100644 --- a/Supabase/src/commonTest/kotlin/StringMaskingTest.kt +++ b/Supabase/src/commonTest/kotlin/StringMaskingTest.kt @@ -1,6 +1,6 @@ -import io.github.jan.supabase.maskHeaders -import io.github.jan.supabase.maskString -import io.github.jan.supabase.maskUrl +import io.github.jan.supabase.StringMasking.maskHeaders +import io.github.jan.supabase.StringMasking.maskString +import io.github.jan.supabase.StringMasking.maskUrl import io.ktor.http.Url import io.ktor.http.headers import kotlin.test.Test