diff --git a/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt index 83c86a91..0901f822 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/CustomHeaderFragment.kt @@ -14,14 +14,19 @@ import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import io.heckel.ntfy.R import io.heckel.ntfy.db.CustomHeader +import io.heckel.ntfy.db.Repository import io.heckel.ntfy.util.AfterChangedTextWatcher import io.heckel.ntfy.util.dangerButton import io.heckel.ntfy.util.validUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class CustomHeaderFragment : DialogFragment() { private var header: CustomHeader? = null - private lateinit var baseUrlsInUse: ArrayList private lateinit var listener: CustomHeaderDialogListener + private lateinit var repository: Repository private lateinit var baseUrlView: TextInputEditText private lateinit var headerNameView: TextInputEditText @@ -38,6 +43,7 @@ class CustomHeaderFragment : DialogFragment() { override fun onAttach(context: Context) { super.onAttach(context) listener = activity as CustomHeaderDialogListener + repository = Repository.getInstance(context) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -50,9 +56,6 @@ class CustomHeaderFragment : DialogFragment() { header = CustomHeader(baseUrl, headerName, headerValue) } - // Required for validation - baseUrlsInUse = arguments?.getStringArrayList(BUNDLE_BASE_URLS_IN_USE) ?: arrayListOf() - // Build root view val view = requireActivity().layoutInflater.inflate(R.layout.fragment_custom_header_dialog, null) @@ -161,29 +164,44 @@ class CustomHeaderFragment : DialogFragment() { // Clear previous errors headerNameLayout.error = null - // Validate header name - var isValid = true - if (headerName.isNotEmpty()) { - if (!validateHeaderName(headerName)) { - headerNameLayout.error = getString(R.string.custom_headers_invalid_name) - isValid = false - } else if (isReservedHeader(headerName)) { - headerNameLayout.error = getString(R.string.custom_headers_reserved_name) - isValid = false + // Validate header name and check if a user already exists for this server + CoroutineScope(Dispatchers.Main).launch { + var isValid = true + if (headerName.isNotEmpty()) { + if (!validateHeaderName(headerName)) { + headerNameLayout.error = getString(R.string.custom_headers_invalid_name) + isValid = false + } else if (isReservedHeader(headerName)) { + headerNameLayout.error = getString(R.string.custom_headers_reserved_name) + isValid = false + } else if (headerName.equals("Authorization", ignoreCase = true)) { + // Check if a user exists for this server (async) + val targetBaseUrl = if (header != null) header!!.baseUrl else baseUrl + val userExists = if (this@CustomHeaderFragment::repository.isInitialized && validUrl(targetBaseUrl)) { + withContext(Dispatchers.IO) { + repository.getUser(targetBaseUrl) != null + } + } else { + false + } + if (userExists) { + headerNameLayout.error = getString(R.string.custom_headers_user_exists) + isValid = false + } + } + } + if (header == null) { + // New header: baseUrl, name, and value required + positiveButton.isEnabled = validUrl(baseUrl) + && headerName.isNotEmpty() + && headerValue.isNotEmpty() + && isValid + } else { + // Editing header: name and value required + positiveButton.isEnabled = headerName.isNotEmpty() + && headerValue.isNotEmpty() + && isValid } - } - - if (header == null) { - // New header: baseUrl, name, and value required - positiveButton.isEnabled = validUrl(baseUrl) - && headerName.isNotEmpty() - && headerValue.isNotEmpty() - && isValid - } else { - // Editing header: name and value required - positiveButton.isEnabled = headerName.isNotEmpty() - && headerValue.isNotEmpty() - && isValid } } @@ -215,12 +233,10 @@ class CustomHeaderFragment : DialogFragment() { private const val BUNDLE_BASE_URL = "baseUrl" private const val BUNDLE_HEADER_NAME = "headerName" private const val BUNDLE_HEADER_VALUE = "headerValue" - private const val BUNDLE_BASE_URLS_IN_USE = "baseUrlsInUse" - fun newInstance(header: CustomHeader?, baseUrlsInUse: List): CustomHeaderFragment { + fun newInstance(header: CustomHeader?): CustomHeaderFragment { val fragment = CustomHeaderFragment() val args = Bundle() - args.putStringArrayList(BUNDLE_BASE_URLS_IN_USE, ArrayList(baseUrlsInUse)) if (header != null) { args.putString(BUNDLE_BASE_URL, header.baseUrl) args.putString(BUNDLE_HEADER_NAME, header.name) diff --git a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt index 09dc77ec..5e584edf 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/SettingsActivity.kt @@ -980,8 +980,6 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere } private fun addCustomHeaderPreferences(headersByBaseUrl: List) { - val baseUrlsInUse = headersByBaseUrl.map { it.baseUrl } - headersByBaseUrl.forEach { serverHeaders -> val baseUrl = serverHeaders.baseUrl val headers = serverHeaders.headers @@ -997,7 +995,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere preference.onPreferenceClickListener = OnPreferenceClickListener { _ -> activity?.let { CustomHeaderFragment - .newInstance(header, baseUrlsInUse) + .newInstance(header) .show(it.supportFragmentManager, CustomHeaderFragment.TAG) } true @@ -1017,7 +1015,7 @@ class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPrefere headerAddPref.onPreferenceClickListener = OnPreferenceClickListener { _ -> activity?.let { CustomHeaderFragment - .newInstance(header = null, baseUrlsInUse = baseUrlsInUse) + .newInstance(header = null) .show(it.supportFragmentManager, CustomHeaderFragment.TAG) } true diff --git a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt index 9c66d076..8ae587b5 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/UserFragment.kt @@ -13,6 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout import io.heckel.ntfy.R +import io.heckel.ntfy.db.Repository import io.heckel.ntfy.db.User import io.heckel.ntfy.util.AfterChangedTextWatcher import io.heckel.ntfy.util.dangerButton @@ -22,6 +23,7 @@ class UserFragment : DialogFragment() { private var user: User? = null private lateinit var baseUrlsInUse: ArrayList private lateinit var listener: UserDialogListener + private lateinit var repository: Repository private lateinit var baseUrlViewLayout: TextInputLayout private lateinit var baseUrlView: TextInputEditText @@ -38,6 +40,7 @@ class UserFragment : DialogFragment() { override fun onAttach(context: Context) { super.onAttach(context) listener = activity as UserDialogListener + repository = Repository.getInstance(context) } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -155,9 +158,26 @@ class UserFragment : DialogFragment() { val baseUrl = baseUrlView.text?.toString() ?: "" val username = usernameView.text?.toString() ?: "" val password = passwordView.text?.toString() ?: "" + + // Clear previous errors + baseUrlViewLayout.error = null + if (user == null) { + // Check if Authorization header already exists in custom headers + val hasAuthorizationHeader = if (this::repository.isInitialized && validUrl(baseUrl)) { + repository.getCustomHeadersForServer(baseUrl) + .any { it.name.equals("Authorization", ignoreCase = true) } + } else { + false + } + + if (hasAuthorizationHeader) { + baseUrlViewLayout.error = getString(R.string.user_dialog_base_url_error_authorization_header_exists) + } + positiveButton.isEnabled = validUrl(baseUrl) && !baseUrlsInUse.contains(baseUrl) + && !hasAuthorizationHeader && username.isNotEmpty() && password.isNotEmpty() } else { positiveButton.isEnabled = username.isNotEmpty() // Unchanged if left blank diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6bc97161..d73bac82 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -223,6 +223,7 @@ Du kannst hier einen Benutzer hinzufügen. Alle Themen auf dem angegebenen Server verwenden dann diesen Benutzer. Du kannst Benutzernamen/Kennwort für den gewählten Benutzer ändern oder löschen. Service-URL + Authorization-Header ist bereits in benutzerdefinierten Headern für diesen Server gesetzt Benutzername Kennwort Kennwort (leer lassen für keine Änderung) @@ -364,6 +365,7 @@ Ungültiger Header Ungültige Zeichen im Header-Namen Dieser Header ist reserviert und wird von ntfy gesetzt + Authorization-Header kann nicht hinzugefügt werden: Ein Benutzer ist bereits für diesen Server konfiguriert Name (z.B. CF-Access-Client-Id) Wert Einen benutzerdefinierten HTTP-Header hinzufügen, der mit jeder Anfrage an den angegebenen Server gesendet wird. diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8c2286ca..1f94ebf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -458,6 +458,7 @@ You can add a user here. All topics for the given server will use this user. You may edit username/password for the selected user, or delete it. Service URL + Authorization header already set in custom headers for this server Username Password Password (unchanged if left blank) @@ -479,6 +480,7 @@ Invalid Header Header name contains invalid characters This header is reserved and set by ntfy + Cannot add Authorization header: A user is already configured for this server Name (e.g. CF-Access-Client-Id) Value Add a custom HTTP header that will be sent with every request to the specified server.