From fce29831a22d28c3bc86ede2b6c06da46fe1c56a Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 14 Sep 2026 19:25:24 +0300 Subject: [PATCH] add giga wear auth ui --- .../backend/data/BackendAuthRepositoryImpl.kt | 9 +- apps/giga-wear/client/build.gradle.kts | 2 + .../gigawear/data/AuthRepositoryImpl.kt | 29 +++ .../gigawear/data/RefreshTokenActionImpl.kt | 19 ++ .../gigawear/data/TokenStorageImpl.kt | 32 +++ .../presentation/GigaWearRootComponent.kt | 2 +- .../gigawear/presentation/MainActivity.kt | 2 + .../presentation/auth/AuthComponent.kt | 75 +++++++ .../gigawear/presentation/auth/AuthScreen.kt | 185 ++++++++++++++++++ .../presentation/chat/ChatComponent.kt | 4 +- .../gigawear/presentation/nav/Child.kt | 2 + .../gigawear/presentation/nav/Route.kt | 4 + .../client/src/main/res/values/strings.xml | 13 ++ .../gigawear/common/AuthRepository.kt | 10 +- gradle/libs.versions.toml | 2 + .../shadowsparky/backend/data/JwtPreparer.kt | 8 +- 16 files changed, 383 insertions(+), 15 deletions(-) create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/AuthRepositoryImpl.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/RefreshTokenActionImpl.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/TokenStorageImpl.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthComponent.kt create mode 100644 apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthScreen.kt diff --git a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/BackendAuthRepositoryImpl.kt b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/BackendAuthRepositoryImpl.kt index 5416771..f0d7e0e 100644 --- a/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/BackendAuthRepositoryImpl.kt +++ b/apps/giga-wear/backend/src/main/kotlin/ru/shadowsparky/gigawear/backend/data/BackendAuthRepositoryImpl.kt @@ -12,6 +12,7 @@ import ru.shadowsparky.domain.DispatcherProvider import ru.shadowsparky.gigawear.backend.AppDatabase import ru.shadowsparky.gigawear.common.AuthRepository import ru.shadowsparky.gigawear.common.LoginRequest +import ru.shadowsparky.gigawear.common.UpdateTokenRequest import ru.shadowsparky.http.domain.BadRequestException import ru.shadowsparky.http.domain.TokenInfo import java.time.ZoneOffset @@ -31,16 +32,14 @@ class BackendAuthRepositoryImpl( private val tokenQueries = appDatabase.refresh_tokensQueries override suspend fun login(request: LoginRequest) = withContext(dispatcherProvider.io) { - val userCode = request.code - val user = usersQueries.selectByCode(userCode).executeAsOneOrNull() + val user = usersQueries.selectByCode(request.code).executeAsOneOrNull() ?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER)) userVerifier.verify(user.user_id) create(user.user_id) } - override suspend fun update(oldTokenInfo: TokenInfo) = withContext(dispatcherProvider.io) { - checkNotNull(oldTokenInfo.refresh) - val tokenHash = oldTokenInfo.refresh!!.toTokenHash(SALT) + override suspend fun update(refreshToken: UpdateTokenRequest) = withContext(dispatcherProvider.io) { + val tokenHash = refreshToken.refreshToken.toTokenHash(SALT) val savedToken = tokenQueries.selectByHash(tokenHash) .executeAsOneOrNull() ?: throw BadRequestException("Token not found") diff --git a/apps/giga-wear/client/build.gradle.kts b/apps/giga-wear/client/build.gradle.kts index 429f88e..65aeaf0 100644 --- a/apps/giga-wear/client/build.gradle.kts +++ b/apps/giga-wear/client/build.gradle.kts @@ -32,6 +32,8 @@ dependencies { implementation(project(":feature:chat:chat-common")) implementation(project(":feature:updater:updater-common")) implementation(project(":feature:updater:updater-client")) + implementation(libs.androidx.wear.input) + implementation(libs.multiplatform.settings) implementation(libs.activity.compose) implementation(libs.androidx.compose.foundation) diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/AuthRepositoryImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/AuthRepositoryImpl.kt new file mode 100644 index 0000000..0a360c4 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/AuthRepositoryImpl.kt @@ -0,0 +1,29 @@ +package ru.shadowsparky.gigawear.data + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import org.koin.core.annotation.Factory +import ru.shadowsparky.gigawear.common.AuthRepository +import ru.shadowsparky.gigawear.common.LoginRequest +import ru.shadowsparky.gigawear.common.UpdateTokenRequest +import ru.shadowsparky.http.domain.TokenInfo + +@Factory +class AuthRepositoryImpl( + private val httpClient: HttpClient +) : AuthRepository { + + override suspend fun login(request: LoginRequest): TokenInfo { + return httpClient.post("$ENDPOINT/${AuthRepository.LOGIN_PATH}") { + setBody(request) + }.body() + } + + override suspend fun update(refreshToken: UpdateTokenRequest): TokenInfo { + return httpClient.post("$ENDPOINT/${AuthRepository.UPDATE_TOKEN_PATH}") { + setBody(refreshToken) + }.body() + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/RefreshTokenActionImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/RefreshTokenActionImpl.kt new file mode 100644 index 0000000..4299204 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/RefreshTokenActionImpl.kt @@ -0,0 +1,19 @@ +package ru.shadowsparky.gigawear.data + +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.post +import io.ktor.client.request.setBody +import org.koin.core.annotation.Factory +import ru.shadowsparky.gigawear.common.AuthRepository +import ru.shadowsparky.http.domain.RefreshTokenAction +import ru.shadowsparky.http.domain.TokenInfo + +@Factory +class RefreshTokenActionImpl : RefreshTokenAction { + override suspend fun refresh(client: HttpClient, refreshToken: String): TokenInfo { + return client.post("$ENDPOINT/${AuthRepository.UPDATE_TOKEN_PATH}") { + setBody(refreshToken) + }.body() + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/TokenStorageImpl.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/TokenStorageImpl.kt new file mode 100644 index 0000000..f86e02d --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/data/TokenStorageImpl.kt @@ -0,0 +1,32 @@ +package ru.shadowsparky.gigawear.data + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json +import org.koin.core.annotation.Single +import ru.shadowsparky.domain.KeyValueStorage +import ru.shadowsparky.http.domain.TokenInfo +import ru.shadowsparky.http.domain.TokenStorage + +@Single +class TokenStorageImpl( + private val keyValueStorage: KeyValueStorage, + private val json: Json +) : TokenStorage { + override val realm: String = "wear" + override val token: Flow = keyValueStorage.get(KEY).map { + it?.let { json.decodeFromString(it) } + } + + override suspend fun updateToken(newToken: TokenInfo) { + keyValueStorage.put(KEY, json.encodeToString(newToken)) + } + + override suspend fun clear() { + keyValueStorage.remove(KEY) + } + + private companion object { + const val KEY = "token" + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt index b326531..795566d 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/GigaWearRootComponent.kt @@ -10,7 +10,7 @@ import ru.shadowsparky.ui.nav.RootComponent class GigaWearRootComponent( context: ComponentContext ) : RootComponent(context) { - override val initStack: List = listOf(Route.ChatScreen()) + override val initStack: List = listOf(Route.AuthScreen()) override val serializer: KSerializer = Route.serializer() override val koin: Koin = ru.shadowsparky.koin } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt index 71454fb..94ecd2b 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/MainActivity.kt @@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import com.arkivanov.decompose.retainedComponent import org.koin.android.ext.android.inject +import ru.shadowsparky.gigawear.presentation.auth.AuthScreen import ru.shadowsparky.gigawear.presentation.chat.ChatScreen import ru.shadowsparky.gigawear.presentation.nav.Child import ru.shadowsparky.ui.nav.Nav @@ -19,6 +20,7 @@ class MainActivity : ComponentActivity() { Nav(root) { child, _ -> when (child) { is Child.MainChild -> ChatScreen(child.component) + is Child.AuthChild -> AuthScreen(child.component) } } } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthComponent.kt new file mode 100644 index 0000000..ac54132 --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthComponent.kt @@ -0,0 +1,75 @@ +package ru.shadowsparky.gigawear.presentation.auth + +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.launch +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Named +import ru.shadowsparky.gigawear.common.AuthRepository +import ru.shadowsparky.gigawear.common.LoginRequest +import ru.shadowsparky.gigawear.presentation.nav.Child +import ru.shadowsparky.gigawear.presentation.nav.Route +import ru.shadowsparky.http.domain.TokenStorage +import ru.shadowsparky.ui.nav.ComponentFactory +import ru.shadowsparky.ui.nav.RouteNavigator + +class AuthComponent( + private val context: ComponentContext, + private val tokenStorage: TokenStorage, + private val authRepository: AuthRepository, + private val goToChat: () -> Unit +) : ComponentContext by context { + private val scope = coroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + private val _state = MutableStateFlow(AuthState()) + val state = _state.asStateFlow() + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + _state.value = AuthState(isLoading = false, error = throwable) + } + + init { + scope.launch(exceptionHandler) { + if (tokenStorage.token.firstOrNull() != null) goToChat() + } + } + + fun login(code: Int) { + scope.launch(exceptionHandler) { + _state.value = AuthState(isLoading = true, error = null) + val token = authRepository.login(LoginRequest(code)) + tokenStorage.updateToken(token) + goToChat() + _state.value = AuthState(isLoading = false, error = null) + } + } +} + +data class AuthState( + val isLoading: Boolean = false, + val error: Throwable? = null +) + +@Factory +@Named(Route.AUTH) +class AuthComponentFactory( + private val tokenStorage: TokenStorage, + private val authRepository: AuthRepository, +) : ComponentFactory { + override fun create( + route: Route.AuthScreen, + child: ComponentContext, + root: RouteNavigator + ): Child.AuthChild { + val component = AuthComponent( + child, + tokenStorage, + authRepository + ) { root.nav(Route.ChatScreen()) } + return Child.AuthChild(component) + } +} diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthScreen.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthScreen.kt new file mode 100644 index 0000000..219f25d --- /dev/null +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/auth/AuthScreen.kt @@ -0,0 +1,185 @@ +package ru.shadowsparky.gigawear.presentation.auth + +import android.app.RemoteInput +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.wear.compose.foundation.lazy.ScalingLazyColumn +import androidx.wear.compose.material3.AppScaffold +import androidx.wear.compose.material3.Button +import androidx.wear.compose.material3.CircularProgressIndicator +import androidx.wear.input.RemoteInputIntentHelper +import ru.shadowsparky.gigawear.R +import androidx.wear.compose.material3.Text as WearText + +private const val CODE_LENGTH = 6 +private const val CODE_INPUT_KEY = "code_input" + +@Composable +fun AuthScreen(component: AuthComponent) { + val state by component.state.collectAsState() + AuthScreen( + state = state, + onLogin = { code -> component.login(code) } + ) +} + +@Composable +private fun AuthScreen( + state: AuthState, + onLogin: (code: Int) -> Unit, + modifier: Modifier = Modifier +) { + var code by rememberSaveable { mutableStateOf("") } + AppScaffold(modifier = modifier) { + CodeStep( + code = code, + onCodeChange = { code = it }, + onSubmit = { + code.toIntOrNull()?.let { onLogin(it) } + }, + isLoading = state.isLoading, + error = state.error + ) + } +} + +@Composable +private fun CodeStep( + code: String, + onCodeChange: (String) -> Unit, + onSubmit: () -> Unit, + isLoading: Boolean, + error: Throwable? +) { + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val text = result.data + ?.let { RemoteInput.getResultsFromIntent(it) } + ?.getCharSequence(CODE_INPUT_KEY) + ?.toString() + ?: return@rememberLauncherForActivityResult + + val clean = text.filter(Char::isDigit).take(CODE_LENGTH) + onCodeChange(clean) + if (clean.length == CODE_LENGTH) { + onSubmit() + } + } + + val openCodeInput = { + val intent = RemoteInputIntentHelper.createActionRemoteInputIntent() + val remoteInputs = listOf( + RemoteInput.Builder(CODE_INPUT_KEY) + .setLabel("Код") + .build() + ) + RemoteInputIntentHelper.putRemoteInputsExtra(intent, remoteInputs) + launcher.launch(intent) + } + + ScalingLazyColumn( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + item { + WearText( + text = stringResource(R.string.auth_code_title), + style = MaterialTheme.typography.bodyMedium + ) + } + item { + CodeDots( + code = code, + onClick = openCodeInput + ) + } + item { + WearText( + text = stringResource( + R.string.auth_code_counter, + code.length, + CODE_LENGTH + ), + style = MaterialTheme.typography.labelMedium + ) + } + item { + Button( + onClick = onSubmit, + enabled = code.length == CODE_LENGTH && !isLoading + ) { + if (isLoading) { + Box(contentAlignment = Alignment.Center) { + CircularProgressIndicator(modifier = Modifier.size(24.dp)) + } + } else { + WearText(stringResource(R.string.auth_submit)) + } + } + } + if (error != null) { + item { + WearText( + text = error.message + ?: stringResource(R.string.auth_error_unknown), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error + ) + } + } + } +} + +@Composable +private fun CodeDots( + code: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(CODE_LENGTH) { index -> + val filled = index < code.length + Box( + modifier = Modifier + .padding(horizontal = 4.dp) + .size(20.dp) + .clip(CircleShape) + .background( + if (filled) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.surfaceVariant + ) + .clickable(onClick = onClick) + ) + } + } +} \ No newline at end of file diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt index a4b1d2a..e04f646 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/chat/ChatComponent.kt @@ -34,9 +34,7 @@ class ChatComponent( private val _events = Channel(Channel.BUFFERED) val events = _events.receiveAsFlow() - init { - updateComponent.checkUpdates(true) - } + init { updateComponent.checkUpdates() } fun onMicClicked() { if (_state.value.isLoading) return diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt index c565958..505e9fa 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Child.kt @@ -1,7 +1,9 @@ package ru.shadowsparky.gigawear.presentation.nav +import ru.shadowsparky.gigawear.presentation.auth.AuthComponent import ru.shadowsparky.gigawear.presentation.chat.ChatComponent sealed class Child : ru.shadowsparky.ui.nav.Child { class MainChild(val component: ChatComponent) : Child() + class AuthChild(val component: AuthComponent) : Child() } diff --git a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt index c60183c..8fdccfc 100644 --- a/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt +++ b/apps/giga-wear/client/src/main/kotlin/ru/shadowsparky/gigawear/presentation/nav/Route.kt @@ -8,7 +8,11 @@ sealed class Route : ru.shadowsparky.ui.nav.Route() { @Serializable data class ChatScreen(override val routeId: String = CHAT) : Route() + @Serializable + data class AuthScreen(override val routeId: String = AUTH) : Route() + companion object { const val CHAT = "chat" + const val AUTH = "auth" } } diff --git a/apps/giga-wear/client/src/main/res/values/strings.xml b/apps/giga-wear/client/src/main/res/values/strings.xml index 11e8683..fddc304 100644 --- a/apps/giga-wear/client/src/main/res/values/strings.xml +++ b/apps/giga-wear/client/src/main/res/values/strings.xml @@ -2,4 +2,17 @@ Свин Wear Введите запрос к ассистенту Произошла ошибка + + + Вход + Логин + Далее + + + Код + %1$d цифр + %1$d / %2$d + Войти + Назад + Ошибка входа diff --git a/apps/giga-wear/common/src/commonMain/kotlin/ru/shadowsparky/gigawear/common/AuthRepository.kt b/apps/giga-wear/common/src/commonMain/kotlin/ru/shadowsparky/gigawear/common/AuthRepository.kt index 8de783d..29cfa4e 100644 --- a/apps/giga-wear/common/src/commonMain/kotlin/ru/shadowsparky/gigawear/common/AuthRepository.kt +++ b/apps/giga-wear/common/src/commonMain/kotlin/ru/shadowsparky/gigawear/common/AuthRepository.kt @@ -5,7 +5,7 @@ import ru.shadowsparky.http.domain.TokenInfo interface AuthRepository { suspend fun login(request: LoginRequest): TokenInfo - suspend fun update(oldTokenInfo: TokenInfo): TokenInfo + suspend fun update(refreshToken: UpdateTokenRequest): TokenInfo companion object { const val LOGIN_PATH = "login" @@ -14,7 +14,9 @@ interface AuthRepository { } @Serializable -data class LoginRequest( - val login: String, - val code: Int +class UpdateTokenRequest( + val refreshToken: String ) + +@Serializable +data class LoginRequest(val code: Int) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e54e48b..33c0537 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -69,6 +69,7 @@ guava = "33.2.1-android" tilesTooling = "1.5.0" tilesToolingPreview = "1.5.0" watchfaceComplicationsDataSourceKtx = "1.2.1" +wearInput = "1.2.0" [libraries] adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" } @@ -189,6 +190,7 @@ guava = { group = "com.google.guava", name = "guava", version.ref = "guava" } androidx-tiles-tooling = { group = "androidx.wear.tiles", name = "tiles-tooling", version.ref = "tilesTooling" } androidx-tiles-tooling-preview = { group = "androidx.wear.tiles", name = "tiles-tooling-preview", version.ref = "tilesToolingPreview" } androidx-watchface-complications-data-source-ktx = { group = "androidx.wear.watchface", name = "watchface-complications-data-source-ktx", version.ref = "watchfaceComplicationsDataSourceKtx" } +androidx-wear-input = { group = "androidx.wear", name = "wear-input", version.ref = "wearInput" } [plugins] jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-version" } diff --git a/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/JwtPreparer.kt b/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/JwtPreparer.kt index 4eb61ba..8af8159 100644 --- a/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/JwtPreparer.kt +++ b/libs/backend-base/src/main/kotlin/ru/shadowsparky/backend/data/JwtPreparer.kt @@ -14,8 +14,12 @@ class JwtPreparer( private val envFetcher: EnvFetcher ) { private val tokenTimeInMinutes = envFetcher.get("JWT_DURATION", "30").toLong() - fun prepare(userId: Long): String { - val exp = Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(tokenTimeInMinutes)) + + fun prepare( + userId: Long, + expMillis: Long = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(tokenTimeInMinutes) + ): String { + val exp = Date(expMillis) val token = JWT.create() .withAudience(jwtInfo.audience) .withIssuer(jwtInfo.issuer)