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