add giga wear auth ui
This commit is contained in:
+4
-5
@@ -12,6 +12,7 @@ import ru.shadowsparky.domain.DispatcherProvider
|
|||||||
import ru.shadowsparky.gigawear.backend.AppDatabase
|
import ru.shadowsparky.gigawear.backend.AppDatabase
|
||||||
import ru.shadowsparky.gigawear.common.AuthRepository
|
import ru.shadowsparky.gigawear.common.AuthRepository
|
||||||
import ru.shadowsparky.gigawear.common.LoginRequest
|
import ru.shadowsparky.gigawear.common.LoginRequest
|
||||||
|
import ru.shadowsparky.gigawear.common.UpdateTokenRequest
|
||||||
import ru.shadowsparky.http.domain.BadRequestException
|
import ru.shadowsparky.http.domain.BadRequestException
|
||||||
import ru.shadowsparky.http.domain.TokenInfo
|
import ru.shadowsparky.http.domain.TokenInfo
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
@@ -31,16 +32,14 @@ class BackendAuthRepositoryImpl(
|
|||||||
private val tokenQueries = appDatabase.refresh_tokensQueries
|
private val tokenQueries = appDatabase.refresh_tokensQueries
|
||||||
|
|
||||||
override suspend fun login(request: LoginRequest) = withContext(dispatcherProvider.io) {
|
override suspend fun login(request: LoginRequest) = withContext(dispatcherProvider.io) {
|
||||||
val userCode = request.code
|
val user = usersQueries.selectByCode(request.code).executeAsOneOrNull()
|
||||||
val user = usersQueries.selectByCode(userCode).executeAsOneOrNull()
|
|
||||||
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
?: throw BadRequestException(stringFetcher.get(StringResource.UNKNOWN_USER))
|
||||||
userVerifier.verify(user.user_id)
|
userVerifier.verify(user.user_id)
|
||||||
create(user.user_id)
|
create(user.user_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun update(oldTokenInfo: TokenInfo) = withContext(dispatcherProvider.io) {
|
override suspend fun update(refreshToken: UpdateTokenRequest) = withContext(dispatcherProvider.io) {
|
||||||
checkNotNull(oldTokenInfo.refresh)
|
val tokenHash = refreshToken.refreshToken.toTokenHash(SALT)
|
||||||
val tokenHash = oldTokenInfo.refresh!!.toTokenHash(SALT)
|
|
||||||
val savedToken = tokenQueries.selectByHash(tokenHash)
|
val savedToken = tokenQueries.selectByHash(tokenHash)
|
||||||
.executeAsOneOrNull()
|
.executeAsOneOrNull()
|
||||||
?: throw BadRequestException("Token not found")
|
?: throw BadRequestException("Token not found")
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ dependencies {
|
|||||||
implementation(project(":feature:chat:chat-common"))
|
implementation(project(":feature:chat:chat-common"))
|
||||||
implementation(project(":feature:updater:updater-common"))
|
implementation(project(":feature:updater:updater-common"))
|
||||||
implementation(project(":feature:updater:updater-client"))
|
implementation(project(":feature:updater:updater-client"))
|
||||||
|
implementation(libs.androidx.wear.input)
|
||||||
|
implementation(libs.multiplatform.settings)
|
||||||
|
|
||||||
implementation(libs.activity.compose)
|
implementation(libs.activity.compose)
|
||||||
implementation(libs.androidx.compose.foundation)
|
implementation(libs.androidx.compose.foundation)
|
||||||
|
|||||||
+29
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -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<TokenInfo?> = 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -10,7 +10,7 @@ import ru.shadowsparky.ui.nav.RootComponent
|
|||||||
class GigaWearRootComponent(
|
class GigaWearRootComponent(
|
||||||
context: ComponentContext
|
context: ComponentContext
|
||||||
) : RootComponent<Route>(context) {
|
) : RootComponent<Route>(context) {
|
||||||
override val initStack: List<Route> = listOf(Route.ChatScreen())
|
override val initStack: List<Route> = listOf(Route.AuthScreen())
|
||||||
override val serializer: KSerializer<Route> = Route.serializer()
|
override val serializer: KSerializer<Route> = Route.serializer()
|
||||||
override val koin: Koin = ru.shadowsparky.koin
|
override val koin: Koin = ru.shadowsparky.koin
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import com.arkivanov.decompose.retainedComponent
|
import com.arkivanov.decompose.retainedComponent
|
||||||
import org.koin.android.ext.android.inject
|
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.chat.ChatScreen
|
||||||
import ru.shadowsparky.gigawear.presentation.nav.Child
|
import ru.shadowsparky.gigawear.presentation.nav.Child
|
||||||
import ru.shadowsparky.ui.nav.Nav
|
import ru.shadowsparky.ui.nav.Nav
|
||||||
@@ -19,6 +20,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
Nav(root) { child, _ ->
|
Nav(root) { child, _ ->
|
||||||
when (child) {
|
when (child) {
|
||||||
is Child.MainChild -> ChatScreen(child.component)
|
is Child.MainChild -> ChatScreen(child.component)
|
||||||
|
is Child.AuthChild -> AuthScreen(child.component)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+75
@@ -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<Child.AuthChild, Route.AuthScreen, Route> {
|
||||||
|
override fun create(
|
||||||
|
route: Route.AuthScreen,
|
||||||
|
child: ComponentContext,
|
||||||
|
root: RouteNavigator<Route>
|
||||||
|
): Child.AuthChild {
|
||||||
|
val component = AuthComponent(
|
||||||
|
child,
|
||||||
|
tokenStorage,
|
||||||
|
authRepository
|
||||||
|
) { root.nav(Route.ChatScreen()) }
|
||||||
|
return Child.AuthChild(component)
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-3
@@ -34,9 +34,7 @@ class ChatComponent(
|
|||||||
private val _events = Channel<ComponentEvent>(Channel.BUFFERED)
|
private val _events = Channel<ComponentEvent>(Channel.BUFFERED)
|
||||||
val events = _events.receiveAsFlow()
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
init {
|
init { updateComponent.checkUpdates() }
|
||||||
updateComponent.checkUpdates(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onMicClicked() {
|
fun onMicClicked() {
|
||||||
if (_state.value.isLoading) return
|
if (_state.value.isLoading) return
|
||||||
|
|||||||
+2
@@ -1,7 +1,9 @@
|
|||||||
package ru.shadowsparky.gigawear.presentation.nav
|
package ru.shadowsparky.gigawear.presentation.nav
|
||||||
|
|
||||||
|
import ru.shadowsparky.gigawear.presentation.auth.AuthComponent
|
||||||
import ru.shadowsparky.gigawear.presentation.chat.ChatComponent
|
import ru.shadowsparky.gigawear.presentation.chat.ChatComponent
|
||||||
|
|
||||||
sealed class Child : ru.shadowsparky.ui.nav.Child {
|
sealed class Child : ru.shadowsparky.ui.nav.Child {
|
||||||
class MainChild(val component: ChatComponent) : Child()
|
class MainChild(val component: ChatComponent) : Child()
|
||||||
|
class AuthChild(val component: AuthComponent) : Child()
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -8,7 +8,11 @@ sealed class Route : ru.shadowsparky.ui.nav.Route() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class ChatScreen(override val routeId: String = CHAT) : Route()
|
data class ChatScreen(override val routeId: String = CHAT) : Route()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AuthScreen(override val routeId: String = AUTH) : Route()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val CHAT = "chat"
|
const val CHAT = "chat"
|
||||||
|
const val AUTH = "auth"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,4 +2,17 @@
|
|||||||
<string name="app_name">Свин Wear</string>
|
<string name="app_name">Свин Wear</string>
|
||||||
<string name="input_text_to_assist">Введите запрос к ассистенту</string>
|
<string name="input_text_to_assist">Введите запрос к ассистенту</string>
|
||||||
<string name="error_title">Произошла ошибка</string>
|
<string name="error_title">Произошла ошибка</string>
|
||||||
|
|
||||||
|
<!-- Экран входа: шаг логина -->
|
||||||
|
<string name="auth_title">Вход</string>
|
||||||
|
<string name="auth_login_label">Логин</string>
|
||||||
|
<string name="auth_next">Далее</string>
|
||||||
|
|
||||||
|
<!-- Экран входа: шаг кода -->
|
||||||
|
<string name="auth_code_title">Код</string>
|
||||||
|
<string name="auth_code_label">%1$d цифр</string>
|
||||||
|
<string name="auth_code_counter">%1$d / %2$d</string>
|
||||||
|
<string name="auth_submit">Войти</string>
|
||||||
|
<string name="auth_back">Назад</string>
|
||||||
|
<string name="auth_error_unknown">Ошибка входа</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
+6
-4
@@ -5,7 +5,7 @@ import ru.shadowsparky.http.domain.TokenInfo
|
|||||||
|
|
||||||
interface AuthRepository {
|
interface AuthRepository {
|
||||||
suspend fun login(request: LoginRequest): TokenInfo
|
suspend fun login(request: LoginRequest): TokenInfo
|
||||||
suspend fun update(oldTokenInfo: TokenInfo): TokenInfo
|
suspend fun update(refreshToken: UpdateTokenRequest): TokenInfo
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val LOGIN_PATH = "login"
|
const val LOGIN_PATH = "login"
|
||||||
@@ -14,7 +14,9 @@ interface AuthRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class LoginRequest(
|
class UpdateTokenRequest(
|
||||||
val login: String,
|
val refreshToken: String
|
||||||
val code: Int
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class LoginRequest(val code: Int)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ guava = "33.2.1-android"
|
|||||||
tilesTooling = "1.5.0"
|
tilesTooling = "1.5.0"
|
||||||
tilesToolingPreview = "1.5.0"
|
tilesToolingPreview = "1.5.0"
|
||||||
watchfaceComplicationsDataSourceKtx = "1.2.1"
|
watchfaceComplicationsDataSourceKtx = "1.2.1"
|
||||||
|
wearInput = "1.2.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
adaptive-layout = { module = "org.jetbrains.compose.material3.adaptive:adaptive-layout", version.ref = "adaptiveLayout" }
|
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 = { 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-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-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]
|
[plugins]
|
||||||
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-version" }
|
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-version" }
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ class JwtPreparer(
|
|||||||
private val envFetcher: EnvFetcher
|
private val envFetcher: EnvFetcher
|
||||||
) {
|
) {
|
||||||
private val tokenTimeInMinutes = envFetcher.get("JWT_DURATION", "30").toLong()
|
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()
|
val token = JWT.create()
|
||||||
.withAudience(jwtInfo.audience)
|
.withAudience(jwtInfo.audience)
|
||||||
.withIssuer(jwtInfo.issuer)
|
.withIssuer(jwtInfo.issuer)
|
||||||
|
|||||||
Reference in New Issue
Block a user