Skip to content

Commit

Permalink
Refactor SessionStatus (#725)
Browse files Browse the repository at this point in the history
* Fix custom headers on DB functions

* remove files

* Update PostgrestRpc.kt

* Update version

* Ignore 5XX http status codes for session refreshes

* Fix detekt

* Refactor SessionStatus

* Fix detekt

* Revert a docs change

* Revert another docs change

* Fix some doc issues
  • Loading branch information
jan-tennert authored Sep 20, 2024
1 parent 5e42c27 commit 5de19c4
Show file tree
Hide file tree
Showing 20 changed files with 110 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import io.github.jan.supabase.auth.providers.OAuthProvider
import io.github.jan.supabase.auth.providers.builtin.Email
import io.github.jan.supabase.auth.providers.builtin.Phone
import io.github.jan.supabase.auth.providers.builtin.SSO
import io.github.jan.supabase.auth.status.SessionSource
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.auth.user.UserUpdateBuilder
Expand Down Expand Up @@ -401,7 +403,7 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
/**
* Blocks the current coroutine until the plugin is initialized.
*
* This will make sure that the [SessionStatus] is set to [SessionStatus.Authenticated], [SessionStatus.NotAuthenticated] or [SessionStatus.NetworkError].
* This will make sure that the [SessionStatus] is set to [SessionStatus.Authenticated], [SessionStatus.NotAuthenticated] or [SessionStatus.RefreshError].
*/
suspend fun awaitInitialization()

Expand All @@ -412,7 +414,7 @@ sealed interface Auth : MainPlugin<AuthConfig>, CustomSerializationPlugin {
override val logger: SupabaseLogger = SupabaseClient.createLogger("Supabase-Auth")

/**
* The gotrue api version to use
* The auth api version to use
*/
const val API_VERSION = 1

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ open class AuthConfigDefaults : MainConfig() {
var codeVerifierCache: CodeVerifierCache? = null

/**
* The dispatcher used for all gotrue related network requests
* The dispatcher used for all auth related network requests
*/
var coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import io.github.jan.supabase.logging.d
import kotlinx.serialization.json.jsonObject

internal fun noDeeplinkError(arg: String): Nothing = error("""
Trying to use a deeplink as a redirect url, but no deeplink $arg is set in the GoTrueConfig.
If you want to use deep linking, set the scheme and host in the GoTrueConfig:
install(GoTrue) {
Trying to use a deeplink as a redirect url, but no deeplink $arg is set in the AuthConfig.
If you want to use deep linking, set the scheme and host in the AuthConfig:
install(Auth) {
scheme = "YOUR_SCHEME"
host = "YOUR_HOST"
}
Expand Down
27 changes: 19 additions & 8 deletions Auth/src/commonMain/kotlin/io/github/jan/supabase/auth/AuthImpl.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import io.github.jan.supabase.auth.providers.ExternalAuthConfigDefaults
import io.github.jan.supabase.auth.providers.OAuthProvider
import io.github.jan.supabase.auth.providers.builtin.OTP
import io.github.jan.supabase.auth.providers.builtin.SSO
import io.github.jan.supabase.auth.status.RefreshFailureCause
import io.github.jan.supabase.auth.status.SessionSource
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.auth.user.UserUpdateBuilder
Expand Down Expand Up @@ -64,7 +67,7 @@ internal class AuthImpl(
override val config: AuthConfig
) : Auth {

private val _sessionStatus = MutableStateFlow<SessionStatus>(SessionStatus.LoadingFromStorage)
private val _sessionStatus = MutableStateFlow<SessionStatus>(SessionStatus.Initializing)
override val sessionStatus: StateFlow<SessionStatus> = _sessionStatus.asStateFlow()
internal val authScope = CoroutineScope(config.coroutineDispatcher)
override val sessionManager = config.sessionManager ?: createDefaultSessionManager()
Expand Down Expand Up @@ -384,7 +387,7 @@ internal class AuthImpl(
val response = api.postJson("token?grant_type=refresh_token", body) {
headers.remove("Authorization")
}
return response.safeBody("GoTrue#refreshSession")
return response.safeBody("Auth#refreshSession")
}

override suspend fun refreshCurrentSession() {
Expand Down Expand Up @@ -428,18 +431,26 @@ internal class AuthImpl(
}
}

@Suppress("MagicNumber")
private suspend fun tryImportingSession(
importRefreshedSession: suspend () -> Unit,
retry: suspend () -> Unit
) {
try {
importRefreshedSession()
} catch (e: RestException) {
clearSession()
Auth.logger.e(e) { "Couldn't refresh session. The refresh token may have been revoked." }
if (e.statusCode in 500..599) {
Auth.logger.e(e) { "Couldn't refresh session due to an internal server error. Retrying in ${config.retryDelay} (Status code ${e.statusCode})" }
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.InternalServerError(e))
delay(config.retryDelay)
retry()
} else {
Auth.logger.e(e) { "Couldn't refresh session. The refresh token may have been revoked. Clearing session... (Status code ${e.statusCode})" }
clearSession()
}
} catch (e: Exception) {
Auth.logger.e(e) { "Couldn't reach supabase. Either the address doesn't exist or the network might not be on. Retrying in ${config.retryDelay}" }
_sessionStatus.value = SessionStatus.NetworkError
Auth.logger.e(e) { "Couldn't reach Supabase. Either the address doesn't exist or the network might not be on. Retrying in ${config.retryDelay}" }
_sessionStatus.value = SessionStatus.RefreshFailure(RefreshFailureCause.NetworkError(e))
delay(config.retryDelay)
retry()
}
Expand Down Expand Up @@ -558,11 +569,11 @@ internal class AuthImpl(
}

override suspend fun awaitInitialization() {
sessionStatus.first { it !is SessionStatus.LoadingFromStorage }
sessionStatus.first { it !is SessionStatus.Initializing }
}

fun resetLoadingState() {
_sessionStatus.value = SessionStatus.LoadingFromStorage
_sessionStatus.value = SessionStatus.Initializing
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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.status.SessionSource
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.logging.d
import io.ktor.client.request.HttpRequestBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package io.github.jan.supabase.auth.mfa

import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.AuthImpl
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.providers.builtin.Phone
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.UserMfaFactor
import io.github.jan.supabase.auth.user.UserSession
import io.github.jan.supabase.putJsonObject
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.jan.supabase.auth.status

import io.github.jan.supabase.exceptions.RestException

/**
* Represents the cause of a refresh error
*/
sealed interface RefreshFailureCause {

/**
* The refresh failed due to a network error
* @param exception The exception that caused the error
*/
data class NetworkError(val exception: Throwable) : RefreshFailureCause

/**
* The refresh failed due to an internal server error
* @param exception The rest exception that caused the error
*/
data class InternalServerError(val exception: RestException) : RefreshFailureCause

}
Original file line number Diff line number Diff line change
@@ -1,46 +1,9 @@
package io.github.jan.supabase.auth
package io.github.jan.supabase.auth.status

import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.providers.AuthProvider
import io.github.jan.supabase.auth.user.UserSession

/**
* Represents the status of the current session in [Auth]
*/
sealed interface SessionStatus {

/**
* This status means that the user is not logged in
* @param isSignOut Whether this status was caused by a sign out
*/
data class NotAuthenticated(val isSignOut: Boolean) : SessionStatus

/**
* This status means that [Auth] is currently loading the session from storage
*/
data object LoadingFromStorage : SessionStatus

/**
* This status means that [Auth] had an error while refreshing the session
*/
data object NetworkError : SessionStatus

/**
* This status means that [Auth] holds a valid session
* @param session The session
* @param source The source of the session
*/
data class Authenticated(val session: UserSession, val source: SessionSource = SessionSource.Unknown) : SessionStatus {

/**
* Whether the session is new, i.e. [source] is [SessionSource.SignIn], [SessionSource.SignUp] or [SessionSource.External].
* Use this to determine whether this status is the result of a new sign in or sign up or just a session refresh.
*/
val isNew: Boolean = source is SessionSource.SignIn || source is SessionSource.SignUp || source is SessionSource.External

}

}

/**
* Represents the source of a session
*/
Expand All @@ -63,13 +26,13 @@ sealed interface SessionSource {
data class SignIn(val provider: AuthProvider<*, *>) : SessionSource

/**
* The session was loaded from a sign up (only if auto-confirm is enabled)
* The session was loaded from a sign-up (only if auto-confirm is enabled)
* @param provider The provider that was used to sign up
*/
data class SignUp(val provider: AuthProvider<*, *>) : SessionSource

/**
* The session comes from an external source, e.g. OAuth via deeplinks.
* The session comes from an external source, e.g. OAuth via deep links.
*/
data object External : SessionSource

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.jan.supabase.auth.status

import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.user.UserSession

/**
* Represents the status of the current session in [Auth]
*/
sealed interface SessionStatus {

/**
* This status means that the user is not logged in
* @param isSignOut Whether this status was caused by a sign-out
*/
data class NotAuthenticated(val isSignOut: Boolean) : SessionStatus

/**
* This status means that [Auth] is currently initializing the session
*/
data object Initializing : SessionStatus

/**
* This status means that [Auth] had an error while refreshing the session
* @param cause The cause of the error
*/
data class RefreshFailure(val cause: RefreshFailureCause) : SessionStatus

/**
* This status means that [Auth] holds a valid session
* @param session The session
* @param source The source of the session
*/
data class Authenticated(val session: UserSession, val source: SessionSource = SessionSource.Unknown) : SessionStatus {

/**
* Whether the session is new, i.e. [source] is [SessionSource.SignIn], [SessionSource.SignUp] or [SessionSource.External].
* Use this to determine whether this status is the result of a new sign in or sign up or just a session refresh.
*/
val isNew: Boolean = source is SessionSource.SignIn || source is SessionSource.SignUp || source is SessionSource.External

}

}
2 changes: 1 addition & 1 deletion Auth/src/commonTest/kotlin/AuthApiTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import io.github.jan.supabase.auth.AuthConfig
import io.github.jan.supabase.auth.FlowType
import io.github.jan.supabase.auth.OtpType
import io.github.jan.supabase.auth.PKCEConstants
import io.github.jan.supabase.auth.SessionSource
import io.github.jan.supabase.auth.SignOutScope
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.minimalSettings
Expand All @@ -13,6 +12,7 @@ import io.github.jan.supabase.auth.providers.builtin.Email
import io.github.jan.supabase.auth.providers.builtin.IDToken
import io.github.jan.supabase.auth.providers.builtin.OTP
import io.github.jan.supabase.auth.providers.builtin.Phone
import io.github.jan.supabase.auth.status.SessionSource
import io.github.jan.supabase.testing.assertMethodIs
import io.github.jan.supabase.testing.assertPathIs
import io.github.jan.supabase.testing.createMockedSupabaseClient
Expand Down
2 changes: 1 addition & 1 deletion Auth/src/commonTest/kotlin/AuthTest.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import io.github.jan.supabase.SupabaseClientBuilder
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.MemorySessionManager
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.minimalSettings
import io.github.jan.supabase.auth.providers.Github
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.auth.user.Identity
import io.github.jan.supabase.auth.user.UserInfo
import io.github.jan.supabase.auth.user.UserSession
Expand Down
2 changes: 1 addition & 1 deletion Auth/src/commonTest/kotlin/AuthTestUtils.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus

fun Auth.sessionSource() = (sessionStatus.value as SessionStatus.Authenticated).source

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.jan.supabase.auth

import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.status.SessionSource
import io.ktor.util.PlatformUtils.IS_BROWSER
import kotlinx.browser.window
import kotlinx.coroutines.launch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.jan.supabase.realtime
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.annotations.SupabaseInternal
import io.github.jan.supabase.auth.Auth
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.buildUrl
import io.github.jan.supabase.collections.AtomicMutableMap
import io.github.jan.supabase.exceptions.RestException
Expand Down
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/KotlinTargets.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import org.gradle.kotlin.dsl.assign
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

fun KotlinMultiplatformExtension.iosTargets() {
iosX64()
Expand Down Expand Up @@ -67,6 +67,7 @@ fun KotlinMultiplatformExtension.jsTarget() {
}
}

@OptIn(org.jetbrains.kotlin.gradle.ExperimentalWasmDsl::class)
fun KotlinMultiplatformExtension.wasmJsTarget() {
wasmJs {
browser {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package io.github.jan.supabase.compose.auth

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.SupabaseSerializer
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.providers.Apple
import io.github.jan.supabase.auth.providers.Google
import io.github.jan.supabase.auth.providers.IDTokenProvider
import io.github.jan.supabase.auth.providers.builtin.IDToken
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.compose.auth.composable.NativeSignInState
import io.github.jan.supabase.logging.SupabaseLogger
import io.github.jan.supabase.logging.d
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.common.ui.screen.ChatScreen
import io.github.jan.supabase.common.ui.screen.LoginScreen

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.github.jan.supabase.common

import co.touchlab.kermit.Logger
import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.common.net.AuthApi
import io.github.jan.supabase.common.net.Message
import io.github.jan.supabase.common.net.MessageApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package io.github.jan.supabase.common.net

import io.github.jan.supabase.SupabaseClient
import io.github.jan.supabase.auth.OtpType
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.auth
import io.github.jan.supabase.auth.providers.Google
import io.github.jan.supabase.auth.providers.builtin.Email
import io.github.jan.supabase.auth.status.SessionStatus
import kotlinx.coroutines.flow.Flow

sealed interface AuthApi {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.jan.supabase.auth.SessionStatus
import io.github.jan.supabase.auth.status.SessionStatus
import io.github.jan.supabase.common.ui.components.AlertDialog
import io.github.jan.supabase.common.ui.screen.LoginScreen
import io.github.jan.supabase.common.ui.screen.MfaScreen
Expand Down

0 comments on commit 5de19c4

Please sign in to comment.