Внедрение аунтификации

This commit is contained in:
2023-04-17 01:24:41 +03:00
parent d04ceb323f
commit 0ebc9f762b
15 changed files with 357 additions and 112 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
package ru.megboyzz.dnevnik
import android.accounts.Account
import android.app.Application
import androidx.room.Room
import ru.megboyzz.dnevnik.db.CredentialsDataBase
class App : Application() {
var database: CredentialsDataBase? = null
lateinit var database: CredentialsDataBase
private set
override fun onCreate() {
@@ -1,12 +1,17 @@
package ru.megboyzz.dnevnik
import android.app.Application
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.ReportFragment.Companion.reportFragment
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -15,37 +20,19 @@ import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Jsoup
import ru.megboyzz.dnevnik.navigation.AppNavHost
import ru.megboyzz.dnevnik.ui.theme.lightGray
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModelFactory
import java.io.IOException
import java.net.URL
class MainActivity : ComponentActivity() {
lateinit var app: App
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val login = "kitsekaterina"
val password = "k12345"
val returnUrl =
"https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo"
val baseUrl = "https://login.dnevnik.ru/login/"
GlobalScope.launch(Dispatchers.IO) {
val execute: org.jsoup.Connection.Response = Jsoup
.connect(baseUrl)
.method(org.jsoup.Connection.Method.POST)
.data("ReturnUrl", returnUrl)
.data("login", login)
.data("password", password)
.followRedirects(true).execute()
val url: URL = execute.url()
val httpUrl = url.toString().toHttpUrlOrNull()
val queryParameter = httpUrl?.queryParameter("result")
println("$httpUrl $queryParameter")
}
this.app = application as App
setContent {
@@ -57,4 +44,19 @@ class MainActivity : ComponentActivity() {
}
}
}
@OptIn(DelicateCoroutinesApi::class)
override fun onDestroy() {
super.onDestroy()
GlobalScope.launch(Dispatchers.IO) {
val credentialsDao = app.database.credentialsDao()
val credentials = credentialsDao.getCredentials()
if(!credentials.rememberMe)
credentialsDao.deleteCredentials()
}
}
}
+23 -9
View File
@@ -28,10 +28,12 @@ import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarLayoutInfo
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.*
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import ru.megboyzz.dnevnik.navigation.BaseNavRote
import ru.megboyzz.dnevnik.ui.theme.dark
import java.time.DayOfWeek
import kotlin.coroutines.*
@Composable
fun Int.AsPainter() = painterResource(this)
@@ -201,16 +203,28 @@ fun CalendarDay.week(): Week{
}
fun WeekDay.week(): Week{
val weekList = mutableListOf<WeekDay>()
val dayOrder = this.date.dayOfWeek.ordinal
var mondayLocalDate = this.date.minusDays(dayOrder.toLong())
DayOfWeek.values().forEach {
weekList.add(WeekDay(mondayLocalDate, WeekDayPosition.RangeDate))
mondayLocalDate = mondayLocalDate.plusDays(1)
}
return Week(weekList)
private class MutableStateAdapter<T>(
private val state: State<T>,
private val mutate: (T) -> Unit
) : MutableState<T> {
override var value: T
get() = state.value
set(value) {
mutate(value)
}
override fun component1(): T = value
override fun component2(): (T) -> Unit = { value = it }
}
// Flow
@Composable
fun <T> MutableStateFlow<T>.collectAsMutableState(
context: CoroutineContext = EmptyCoroutineContext
): MutableState<T> = MutableStateAdapter(
state = collectAsState(context),
mutate = { value = it }
)
@@ -0,0 +1,110 @@
package ru.megboyzz.dnevnik.authorization
import android.util.Log
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import org.jsoup.Connection
import org.jsoup.Jsoup
import ru.megboyzz.dnevnik.db.dao.CredentialsDao
import ru.megboyzz.dnevnik.entities.db.Credentials
import java.net.URL
class AuthorizationManager(private val credentialsDao: CredentialsDao) {
private val returnUrl =
"https://login.dnevnik.ru/oauth2?response_type=token&client_id=bb97b3e445a340b9b9cab4b9ea0dbd6f&scope=CommonInfo,ContactInfo,FriendsAndRelatives,EducationalInfo"
private val baseUrl = "https://login.dnevnik.ru/login/"
private var credentials = Credentials(token = "", cookies = mapOf(), rememberMe = false)
fun login(
login: String,
password: String,
rememberMe: Boolean
): AuthorizationStatus {
val execute = runCatching<Connection.Response> {
Jsoup
.connect(baseUrl)
.method(Connection.Method.POST)
.data("ReturnUrl", returnUrl)
.data("login", login)
.data("password", password)
.followRedirects(true)
.execute()
}.onFailure {
Log.i("AuthorizationManager", "No internet")
return AuthorizationStatus.NO_INTERNET
}
val response = execute.getOrNull()
if(response != null){
if(response.statusCode() != 200) {
Log.i("AuthorizationManager", "Maintain")
return AuthorizationStatus.MAINTAIN
}
val url = response.url()
if(url.getResult() != "success") {
Log.i("AuthorizationManager", "Wrong login")
return AuthorizationStatus.WRONG_CREDENTIALS
}
//Безопасная зона
val cookies = response.cookies()
credentials = Credentials(
token = url.getToken(),
cookies = cookies,
rememberMe = rememberMe
)
dbUpdate()
}else return AuthorizationStatus.NO_INTERNET //Может быть опасно
return AuthorizationStatus.LOGGED
}
fun checkCredentials(): AuthorizationStatus{
val credentialsIsExists = credentialsDao.credentialsIsExists()
return if(credentialsIsExists)
AuthorizationStatus.LOGGED
else
AuthorizationStatus.NOTHING
}
fun dbUpdate(){
credentialsDao.setCredentials(credentials)
}
fun deleteCredentials(){
credentialsDao.deleteCredentials()
}
fun URL.getResult(): String{
val httpUrl = this.toString().toHttpUrlOrNull() ?: return ""
return httpUrl.queryParameter("result") ?: ""
}
fun URL.getToken(): String{
val tokenRegex = "access_token=([A-Za-z0-9]+)"
val regex = Regex(tokenRegex)
val find = regex.find(this.toString())
if(find != null)
return find.groupValues[1]
return ""
}
}
@@ -0,0 +1,12 @@
package ru.megboyzz.dnevnik.authorization
enum class AuthorizationStatus {
LOGGED,
LOGGING,
WRONG_CREDENTIALS,
NO_INTERNET,
MAINTAIN,
NOTHING
}
@@ -0,0 +1,18 @@
package ru.megboyzz.dnevnik.db.converters
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class CookiesConverter {
private val gson = Gson()
private val mapType = object : TypeToken<Map<String, String>>() {}.type
@TypeConverter
fun fromMap(cookies: Map<String, String>): String = gson.toJson(cookies)
@TypeConverter
fun toMap(savedCookies: String): Map<String, String> = gson.fromJson(savedCookies, mapType)
}
@@ -1,17 +1,22 @@
package ru.megboyzz.dnevnik.db.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ru.megboyzz.dnevnik.entities.db.Credentials
@Dao
interface CredentialsDao {
@Query("SELECT * FROM Credentials WHERE id = 1")
fun getCredentials(): LiveData<Credentials>
@Query("DELETE FROM Credentials WHERE id = 1")
@Query("SELECT EXISTS(SELECT * FROM Credentials WHERE id = 0)")
fun credentialsIsExists(): Boolean
@Query("SELECT * FROM Credentials WHERE id = 0")
fun getCredentials(): Credentials
@Query("DELETE FROM Credentials WHERE id = 0")
fun deleteCredentials()
@Insert(onConflict = OnConflictStrategy.REPLACE)
@@ -1,9 +1,16 @@
package ru.megboyzz.dnevnik.entities.db
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import ru.megboyzz.dnevnik.db.converters.CookiesConverter
@Entity
@TypeConverters(CookiesConverter::class)
data class Credentials(
@PrimaryKey
val id: Int = 0,
val token: String,
val cookies: Map<String, String>
val cookies: Map<String, String>,
val rememberMe: Boolean
)
@@ -1,20 +1,23 @@
package ru.megboyzz.dnevnik.navigation
import android.window.SplashScreen
import androidx.compose.animation.*
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.IntOffset
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import ru.megboyzz.dnevnik.collectAsMutableState
import ru.megboyzz.dnevnik.screens.*
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModelFactory
sealed class BaseNavRote(open val route: String)
@@ -29,18 +32,33 @@ sealed class AppNavRoute(override val route: String): BaseNavRote(route){
object Settings: AppNavRoute("settings_screen")
}
@OptIn(ExperimentalAnimationApi::class)
@Composable
fun AppNavHost(){
val scaffoldState = rememberScaffoldState()
val navController = rememberNavController()
val app = (LocalContext.current as MainActivity).app
val authorizationViewModel: AuthorizationViewModel = viewModel(
factory = AuthorizationViewModelFactory(app)
)
val loginStatus = authorizationViewModel.loginStatus.collectAsMutableState()
NavHost(
navController = navController,
startDestination = AppNavRoute.Login.route /* сделать проверку куки файлов */
startDestination = AppNavRoute.Splash.route
){
composable(AppNavRoute.Splash.route) { }
composable(AppNavRoute.Splash.route) {
if(loginStatus.value != AuthorizationStatus.LOGGED){
navController.navigate(AppNavRoute.Login.route)
}else
navController.navigate(AppNavRoute.Marks.route)
}
composable(AppNavRoute.Login.route) { LoginScreen(navController) }
composable(AppNavRoute.Marks.route) { MarksScreen(navController, scaffoldState) }
composable(AppNavRoute.Schedule.route) { ScheduleScreen(navController, scaffoldState) }
@@ -1,4 +0,0 @@
package ru.megboyzz.dnevnik.repository
class AuthorizationRepository() {
}
@@ -1,35 +1,70 @@
package ru.megboyzz.dnevnik.screens
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import ru.megboyzz.dnevnik.MainActivity
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import ru.megboyzz.dnevnik.collectAsMutableState
import ru.megboyzz.dnevnik.navigation.AppNavRoute
import ru.megboyzz.dnevnik.screens.ui.AlertMessageBox
import ru.megboyzz.dnevnik.screens.ui.LoginScreenContent
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModel
import ru.megboyzz.dnevnik.viewmodel.AuthorizationViewModelFactory
//TODO Переверстать экран входа
@Composable
fun LoginScreen(navController: NavController) {
val login = remember {
mutableStateOf("")
}
val password = remember {
mutableStateOf("")
}
val isError = remember {
mutableStateOf(false)
}
val rememberMe = remember {
mutableStateOf(false)
val app = (LocalContext.current as MainActivity).app
val authorizationViewModel: AuthorizationViewModel = viewModel(
factory = AuthorizationViewModelFactory(app)
)
val login = authorizationViewModel.login.collectAsMutableState()
val password = authorizationViewModel.password.collectAsMutableState()
val loginStatus = authorizationViewModel.loginStatus.collectAsMutableState()
val rememberMe = authorizationViewModel.rememberMe.collectAsMutableState()
var isError by remember{
mutableStateOf(true)
}
if(loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS && isError) {
AlertMessageBox(
title = "Информация",
alertText = "Неверный логин или пароль",
onClick = { isError =! isError }
)
}
if(loginStatus.value == AuthorizationStatus.LOGGED)
navController.navigate(AppNavRoute.Marks.route)
LoginScreenContent(
login = login,
password = password,
isError = isError,
isRememberMe = rememberMe,
onSignIn = { navController.navigate(AppNavRoute.Marks.route) },
login = login.value,
onLoginChange = { login.value = it },
password = password.value,
onPasswordChange = { password.value = it },
isError = loginStatus.value == AuthorizationStatus.WRONG_CREDENTIALS,
onErrorChange = { },
isLoading = loginStatus.value == AuthorizationStatus.LOGGING,
isRememberMe = rememberMe.value,
onRememberChange = { rememberMe.value = it },
onSignIn = {
if(loginStatus.value != AuthorizationStatus.LOGGED)
authorizationViewModel.login()
},
onSignUp = { /*TODO*/ },
onForgetPassword = { /* TODO */ }
)
@@ -33,7 +33,6 @@ import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.DialogProperties
import ru.megboyzz.dnevnik.*
import ru.megboyzz.dnevnik.R
import ru.megboyzz.dnevnik.ui.theme.*
@@ -102,18 +101,20 @@ fun ProfileCard(
@Composable
fun MainTextField(
value: MutableState<String>,
isError: MutableState<Boolean> = remember { mutableStateOf(false) },
value: String,
onChange: (String) -> Unit,
isError: Boolean,
isLoading: Boolean = false,
onErrorChanged: (Boolean) -> Unit,
label: String = "",
keyboardType: KeyboardType = KeyboardType.Text,
onChange: (it: String) -> Unit = { value.value = it }
) {
var passwordVisible by rememberSaveable { mutableStateOf(false) }
val focusRequester = FocusRequester()
OutlinedTextField(
value = value.value,
value = value,
onValueChange = onChange,
label = {
Text(
@@ -126,8 +127,8 @@ fun MainTextField(
textStyle = H2,
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = dark,
unfocusedBorderColor = if (isError.value) falseColor else mainBlue,
focusedBorderColor = if (isError.value) falseColor else mainBlue,
unfocusedBorderColor = if (isError) falseColor else mainBlue,
focusedBorderColor = if (isError) falseColor else mainBlue,
focusedLabelColor = dark,
unfocusedLabelColor = dark,
cursorColor = dark
@@ -152,42 +153,50 @@ fun MainTextField(
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(imageVector = image, "")
}
} else {
if(isLoading) LoginProgress()
}
},
modifier = Modifier
.focusRequester(focusRequester)
.onFocusChanged {
if (it.isFocused)
if (isError.value) isError.value = false
if (isError) onErrorChanged(false)
}
.fillMaxWidth(),
)
}
@Composable
fun LoginProgress() {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
strokeWidth = 2.dp,
color = mainBlue
)
}
@Composable
fun MainCheckbox(
isChecked: MutableState<Boolean>,
isChecked: Boolean,
modifier: Modifier = Modifier,
size: Float = 24f,
checkedColor: Color = mainBlue,
uncheckedColor: Color = lightGray,
checkmarkColor: Color = white,
onValueChange: () -> Unit
onValueChange: (Boolean) -> Unit
) {
val checkboxColor: Color by animateColorAsState(if (isChecked.value) checkedColor else uncheckedColor)
val checkboxColor: Color by animateColorAsState(if (isChecked) checkedColor else uncheckedColor)
val density = LocalDensity.current
val duration = 200
Row(
modifier = modifier
.toggleable(
value = isChecked.value,
value = isChecked,
role = Role.Checkbox,
onValueChange = {
isChecked.value = !isChecked.value
onValueChange.invoke()
}
onValueChange = onValueChange
)
) {
Card(
@@ -202,7 +211,7 @@ fun MainCheckbox(
contentAlignment = Alignment.Center
) {
androidx.compose.animation.AnimatedVisibility(
visible = isChecked.value,
visible = isChecked,
enter = slideInHorizontally(
animationSpec = tween(duration)
) {
@@ -618,7 +627,8 @@ fun AlertMessageBox(
contentColor = white
),
modifier = Modifier
.defaultMinSize(170.dp).padding(bottom = 30.dp)
.defaultMinSize(170.dp)
.padding(bottom = 30.dp)
) {
Text(
text = R.string.title_ok.AsString(),
@@ -22,10 +22,15 @@ import ru.megboyzz.dnevnik.R
@Composable
fun LoginScreenContent(
login: MutableState<String>,
password: MutableState<String>,
isError: MutableState<Boolean>,
isRememberMe: MutableState<Boolean>,
login: String,
onLoginChange: (String) -> Unit,
password: String,
onPasswordChange: (String) -> Unit,
isLoading: Boolean,
isError: Boolean,
onErrorChange: (Boolean) -> Unit,
isRememberMe: Boolean,
onRememberChange: (Boolean) -> Unit,
onSignIn: () -> Unit,
onSignUp: () -> Unit,
onForgetPassword: () -> Unit
@@ -65,14 +70,19 @@ fun LoginScreenContent(
MainTextField(
value = login,
label = R.string.title_login.AsString(),
isError = isError
isError = isError,
isLoading = isLoading,
onErrorChanged = onErrorChange,
onChange = onLoginChange
)
SpacerHeight(15.dp)
MainTextField(
value = password,
keyboardType = KeyboardType.Password,
label = R.string.title_password.AsString(),
isError = isError
isError = isError,
onErrorChanged = onErrorChange,
onChange = onPasswordChange
)
}
SpacerHeight(5.dp)
@@ -87,7 +97,10 @@ fun LoginScreenContent(
modifier = Modifier.fillMaxHeight(),
verticalAlignment = Alignment.CenterVertically
) {
MainCheckbox(isRememberMe) { }
MainCheckbox(
isChecked = isRememberMe,
onValueChange = onRememberChange
)
SpacerWidth(5.dp)
Text(
text = R.string.title_remember_me.AsString(),
@@ -1,7 +0,0 @@
package ru.megboyzz.dnevnik.service
enum class AuthorizationStatus {
WRONG_CREDENTIALS,
NO_INTERNET,
MAINTAIN
}
@@ -1,36 +1,48 @@
package ru.megboyzz.dnevnik.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import ru.megboyzz.dnevnik.App
import ru.megboyzz.dnevnik.authorization.AuthorizationManager
import ru.megboyzz.dnevnik.authorization.AuthorizationStatus
import java.lang.IllegalArgumentException
//Если есть попадание в этот вьюмодел то нам нужна авторизация
class AuthorizationViewModel(application: Application) : AndroidViewModel(application) {
class AuthorizationViewModel(application: App) : AndroidViewModel(application) {
val login = MutableStateFlow("")
val password = MutableStateFlow("")
val isLogged = MutableStateFlow(false)
val rememberMe = MutableStateFlow(false)
val loginStatus = MutableStateFlow(AuthorizationStatus.NOTHING)
private val authManager = AuthorizationManager(application.database.credentialsDao())
init {
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(authManager.checkCredentials())
}
}
fun login(){
viewModelScope.launch(Dispatchers.IO) {
loginStatus.emit(AuthorizationStatus.LOGGING)
loginStatus.emit(authManager.login(login.value, password.value, rememberMe.value))
}
}
}
class AuthorizationViewModelFactory(private val application: Application) : ViewModelProvider.Factory {
class AuthorizationViewModelFactory(private val application: App) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(AuthorizationViewModel::class.java))