mirror of https://github.com/tasks/tasks
Generate app passwords for Tasks.org
parent
9dfdeaa582
commit
16ae98f9eb
@ -0,0 +1,75 @@
|
||||
package org.tasks.caldav
|
||||
|
||||
import at.bitfire.cert4android.CustomCertManager
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.*
|
||||
import org.json.JSONObject
|
||||
|
||||
class TasksClient constructor(
|
||||
provider: CaldavClientProvider,
|
||||
certManager: CustomCertManager,
|
||||
httpClient: OkHttpClient,
|
||||
private val httpUrl: HttpUrl?
|
||||
) : CaldavClient(provider, certManager, httpClient, httpUrl) {
|
||||
suspend fun generateNewPassword(description: String?): JSONObject? = withContext(Dispatchers.IO) {
|
||||
val url = httpUrl?.resolve(ENDPOINT_PASSWORDS) ?: return@withContext null
|
||||
httpClient
|
||||
.newCall(Request.Builder()
|
||||
.post(FormBody.Builder()
|
||||
.apply {
|
||||
if (!description.isNullOrBlank()) {
|
||||
add(FORM_DESCRIPTION, description)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
)
|
||||
.url(url)
|
||||
.build())
|
||||
.execute()
|
||||
.use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
response.body?.use { body -> JSONObject(body.string()) }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deletePassword(id: Int) = withContext(Dispatchers.IO) {
|
||||
val url = httpUrl?.resolve(ENDPOINT_PASSWORDS) ?: return@withContext false
|
||||
httpClient
|
||||
.newCall(Request.Builder()
|
||||
.delete(FormBody.Builder().add(FORM_SESSION_ID, id.toString()).build())
|
||||
.url(url)
|
||||
.build())
|
||||
.execute()
|
||||
.use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAppPasswords(): JSONObject? = withContext(Dispatchers.IO) {
|
||||
val url = httpUrl?.resolve(ENDPOINT_PASSWORDS) ?: return@withContext null
|
||||
httpClient
|
||||
.newCall(Request.Builder()
|
||||
.get()
|
||||
.url(url)
|
||||
.build())
|
||||
.execute()
|
||||
.use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw HttpException(response)
|
||||
}
|
||||
response.body?.use { body -> JSONObject(body.string()) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ENDPOINT_PASSWORDS = "/app-passwords"
|
||||
private const val FORM_DESCRIPTION = "description"
|
||||
private const val FORM_SESSION_ID = "session_id"
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package org.tasks.preferences.fragments
|
||||
|
||||
import androidx.hilt.lifecycle.ViewModelInject
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
import org.tasks.caldav.CaldavClientProvider
|
||||
import org.tasks.data.CaldavAccount
|
||||
import timber.log.Timber
|
||||
|
||||
class TasksAccountViewModel @ViewModelInject constructor(
|
||||
private val provider: CaldavClientProvider
|
||||
) : ViewModel() {
|
||||
val newPassword = MutableLiveData<AppPassword?>()
|
||||
val appPasswords = MutableLiveData<List<AppPassword>?>()
|
||||
|
||||
private var inFlight = false
|
||||
|
||||
fun refreshPasswords(account: CaldavAccount) = viewModelScope.launch {
|
||||
try {
|
||||
provider
|
||||
.forTasksAccount(account)
|
||||
.getAppPasswords()
|
||||
?.let {
|
||||
val passwords = it.getJSONArray(PASSWORDS)
|
||||
val result = ArrayList<AppPassword>()
|
||||
for (i in 0 until passwords.length()) {
|
||||
with(passwords.getJSONObject(i)) {
|
||||
result.add(AppPassword(
|
||||
description = getStringOrNull(DESCRIPTION),
|
||||
id = getInt(SESSION_ID),
|
||||
createdAt = getLongOrNull(CREATED_AT),
|
||||
lastAccess = getLongOrNull(LAST_ACCESS)
|
||||
))
|
||||
}
|
||||
}
|
||||
appPasswords.value = result
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun requestNewPassword(account: CaldavAccount, description: String) = viewModelScope.launch {
|
||||
if (inFlight) {
|
||||
return@launch
|
||||
}
|
||||
inFlight = true
|
||||
try {
|
||||
provider
|
||||
.forTasksAccount(account)
|
||||
.generateNewPassword(description.takeIf { it.isNotBlank() })
|
||||
?.let {
|
||||
newPassword.value =
|
||||
AppPassword(
|
||||
username = it.getString(USERNAME),
|
||||
password = it.getString(PASSWORD)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
inFlight = false
|
||||
}
|
||||
|
||||
fun deletePassword(account: CaldavAccount, id: Int) = viewModelScope.launch {
|
||||
try {
|
||||
provider.forTasksAccount(account).deletePassword(id)
|
||||
refreshPasswords(account)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearNewPassword() {
|
||||
newPassword.value = null
|
||||
}
|
||||
|
||||
data class AppPassword(
|
||||
val username: String? = null,
|
||||
val password: String? = null,
|
||||
val description: String? = null,
|
||||
val id: Int = -1,
|
||||
val createdAt: Long? = null,
|
||||
val lastAccess: Long? = null
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val PASSWORDS = "passwords"
|
||||
private const val DESCRIPTION = "description"
|
||||
private const val SESSION_ID = "session_id"
|
||||
private const val CREATED_AT = "created_at"
|
||||
private const val LAST_ACCESS = "last_access"
|
||||
private const val PASSWORD = "password"
|
||||
private const val USERNAME = "username"
|
||||
|
||||
fun JSONObject.getStringOrNull(key: String) = if (isNull(key)) null else getString(key)
|
||||
|
||||
fun JSONObject.getLongOrNull(key: String) = if (isNull(key)) null else getLong(key)
|
||||
}
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/keyline_first">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/app_password_save"
|
||||
android:textAlignment="center"
|
||||
android:paddingBottom="@dimen/keyline_first" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/url_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/keyline_first"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_content_copy_24px">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:hint="@string/url" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/user_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="@dimen/keyline_first"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_content_copy_24px">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/username"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:hint="@string/user" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/password_layout"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:endIconMode="custom"
|
||||
app:endIconDrawable="@drawable/ic_content_copy_24px">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:editable="false"
|
||||
android:hint="@string/password" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
Loading…
Reference in New Issue