mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-07-30 08:09:21 +00:00
feat(auth): register official client devices
This commit is contained in:
parent
8937dc7b6b
commit
b3f22a6be2
5 changed files with 169 additions and 0 deletions
|
|
@ -0,0 +1,19 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import android.os.Build
|
||||
|
||||
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
|
||||
val deviceName = formatDeviceName(
|
||||
manufacturer = Build.MANUFACTURER.orEmpty(),
|
||||
model = Build.MODEL.orEmpty(),
|
||||
fallback = "Android device",
|
||||
)
|
||||
val osVersion = Build.VERSION.RELEASE.orEmpty()
|
||||
.trim()
|
||||
.ifBlank { Build.VERSION.SDK_INT.toString() }
|
||||
|
||||
return DeviceClientMetadata(
|
||||
deviceName = deviceName,
|
||||
platform = "Android $osVersion",
|
||||
)
|
||||
}
|
||||
|
|
@ -86,6 +86,7 @@ import coil3.svg.SvgDecoder
|
|||
import com.nuvio.app.core.build.AppFeaturePolicy
|
||||
import com.nuvio.app.core.auth.AuthRepository
|
||||
import com.nuvio.app.core.auth.AuthState
|
||||
import com.nuvio.app.core.auth.DeviceSessionRegistration
|
||||
import com.nuvio.app.core.deeplink.AppDeepLink
|
||||
import com.nuvio.app.core.deeplink.AppDeepLinkRepository
|
||||
import com.nuvio.app.core.network.NetworkCondition
|
||||
|
|
@ -488,6 +489,11 @@ fun App(
|
|||
NetworkStatusRepository.uiState
|
||||
}.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(authState) {
|
||||
if (!ownsAppRuntime) return@LaunchedEffect
|
||||
DeviceSessionRegistration.registerIfAuthenticated(force = true)
|
||||
}
|
||||
|
||||
LaunchedEffect(
|
||||
profileState.activeProfile?.profileIndex,
|
||||
profileState.activeProfile?.name,
|
||||
|
|
@ -1051,6 +1057,7 @@ private fun MainAppContent(
|
|||
if (!ownsAppRuntime) return@LaunchedEffect
|
||||
AppForegroundMonitor.events().collect {
|
||||
NetworkStatusRepository.requestForegroundRefresh()
|
||||
DeviceSessionRegistration.registerIfAuthenticated()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.nuvio.app.core.build.AppVersionConfig
|
||||
import com.nuvio.app.core.network.SupabaseProvider
|
||||
import com.nuvio.app.core.sync.SyncClientIdentity
|
||||
import io.github.jan.supabase.postgrest.postgrest
|
||||
import io.github.jan.supabase.postgrest.rpc
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.buildJsonObject
|
||||
import kotlinx.serialization.json.put
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.TimeMark
|
||||
import kotlin.time.TimeSource
|
||||
|
||||
private const val CLIENT_NAME = "Nuvio Mobile"
|
||||
private val REGISTRATION_INTERVAL = 15.minutes
|
||||
|
||||
internal data class DeviceClientMetadata(
|
||||
val deviceName: String,
|
||||
val platform: String,
|
||||
)
|
||||
|
||||
internal expect fun currentDeviceClientMetadata(): DeviceClientMetadata
|
||||
|
||||
object DeviceSessionRegistration {
|
||||
private val log = Logger.withTag("DeviceSessionRegistration")
|
||||
private val registrationMutex = Mutex()
|
||||
private var lastRegistration: TimeMark? = null
|
||||
|
||||
suspend fun registerIfAuthenticated(force: Boolean = false): Boolean {
|
||||
val authState = AuthRepository.state.value as? AuthState.Authenticated ?: return false
|
||||
if (authState.isAnonymous) return false
|
||||
|
||||
return registrationMutex.withLock {
|
||||
if (!force && lastRegistration?.elapsedNow()?.let { it < REGISTRATION_INTERVAL } == true) {
|
||||
return@withLock true
|
||||
}
|
||||
|
||||
val metadata = currentDeviceClientMetadata()
|
||||
val params = buildDeviceRegistrationParams(
|
||||
installationId = SyncClientIdentity.currentClientId(),
|
||||
clientVersion = AppVersionConfig.VERSION_NAME,
|
||||
metadata = metadata,
|
||||
)
|
||||
|
||||
try {
|
||||
SupabaseProvider.client.postgrest.rpc("register_current_device", params)
|
||||
lastRegistration = TimeSource.Monotonic.markNow()
|
||||
true
|
||||
} catch (error: CancellationException) {
|
||||
throw error
|
||||
} catch (error: Throwable) {
|
||||
log.w(error) { "Device session registration failed" }
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun buildDeviceRegistrationParams(
|
||||
installationId: String,
|
||||
clientVersion: String,
|
||||
metadata: DeviceClientMetadata,
|
||||
): JsonObject = buildJsonObject {
|
||||
put("p_installation_id", installationId)
|
||||
put("p_client_name", CLIENT_NAME)
|
||||
put("p_client_version", clientVersion)
|
||||
put("p_platform", metadata.platform)
|
||||
put("p_device_name", metadata.deviceName)
|
||||
}
|
||||
|
||||
internal fun formatDeviceName(
|
||||
manufacturer: String,
|
||||
model: String,
|
||||
fallback: String,
|
||||
): String {
|
||||
val normalizedManufacturer = manufacturer.trim()
|
||||
val normalizedModel = model.trim()
|
||||
|
||||
return when {
|
||||
normalizedModel.isBlank() -> fallback
|
||||
normalizedManufacturer.isBlank() -> normalizedModel
|
||||
normalizedModel.startsWith(normalizedManufacturer, ignoreCase = true) -> normalizedModel
|
||||
else -> "$normalizedManufacturer $normalizedModel"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DeviceSessionRegistrationTest {
|
||||
@Test
|
||||
fun buildsOfficialClientRegistrationPayload() {
|
||||
val params = buildDeviceRegistrationParams(
|
||||
installationId = "nuvio-mobile-installation",
|
||||
clientVersion = "1.2.3",
|
||||
metadata = DeviceClientMetadata(
|
||||
deviceName = "Google Pixel 9",
|
||||
platform = "Android 16",
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals("nuvio-mobile-installation", params.getValue("p_installation_id").toString().trim('"'))
|
||||
assertEquals("Nuvio Mobile", params.getValue("p_client_name").toString().trim('"'))
|
||||
assertEquals("1.2.3", params.getValue("p_client_version").toString().trim('"'))
|
||||
assertEquals("Android 16", params.getValue("p_platform").toString().trim('"'))
|
||||
assertEquals("Google Pixel 9", params.getValue("p_device_name").toString().trim('"'))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun combinesManufacturerAndModelWithoutRepeatingManufacturer() {
|
||||
assertEquals(
|
||||
"Samsung SM-S918B",
|
||||
formatDeviceName("Samsung", "SM-S918B", "Android device"),
|
||||
)
|
||||
assertEquals(
|
||||
"Google Pixel 9",
|
||||
formatDeviceName("Google", "Google Pixel 9", "Android device"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package com.nuvio.app.core.auth
|
||||
|
||||
import platform.UIKit.UIDevice
|
||||
|
||||
internal actual fun currentDeviceClientMetadata(): DeviceClientMetadata {
|
||||
val device = UIDevice.currentDevice
|
||||
val deviceName = device.name
|
||||
.trim()
|
||||
.ifBlank { device.localizedModel.trim() }
|
||||
.ifBlank { "Apple device" }
|
||||
val platform = "${device.systemName()} ${device.systemVersion}".trim()
|
||||
|
||||
return DeviceClientMetadata(
|
||||
deviceName = deviceName,
|
||||
platform = platform,
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue