From fa425a00aabd18d5060bb28560e2a444cbf031aa Mon Sep 17 00:00:00 2001 From: mhdzumair Date: Sun, 15 Dec 2024 00:16:58 +0530 Subject: [PATCH] Add support for preserving and resetting sensitive credentials config fields Introduced mechanisms to handle preconfigured sensitive fields with masking and reset options in the UI. Extended User functionality to reuse existing encrypted configuration for sensitive fields when editing. Improved styling and user experience for managing credentials and secure configurations. --- api/main.py | 73 ++++++++++++++++++++++++++++++++--- api/middleware.py | 4 ++ resources/css/styles.css | 12 ++++++ resources/html/configure.html | 13 +++++-- resources/js/config_script.js | 60 ++++++++++++++++++++++++++-- 5 files changed, 148 insertions(+), 14 deletions(-) diff --git a/api/main.py b/api/main.py index 069b698..542f2f5 100644 --- a/api/main.py +++ b/api/main.py @@ -166,21 +166,33 @@ async def configure( request: Request, user_data: schemas.UserData = Depends(get_user_data), kodi_code: str = None, + secret_str: str = None, ): response.headers.update(const.NO_CACHE_HEADERS) + configured_fields = [] # Remove the password from the streaming provider if user_data.streaming_provider: - user_data.streaming_provider.password = None - user_data.streaming_provider.token = None + user_data.streaming_provider.password = "••••••••" + user_data.streaming_provider.token = "••••••••" + configured_fields.extend(["provider_token", "password"]) if user_data.streaming_provider.qbittorrent_config: - user_data.streaming_provider.qbittorrent_config.qbittorrent_password = None - user_data.streaming_provider.qbittorrent_config.webdav_password = None + user_data.streaming_provider.qbittorrent_config.qbittorrent_password = ( + "••••••••" + ) + user_data.streaming_provider.qbittorrent_config.webdav_password = "••••••••" + configured_fields.extend(["qbittorrent_password", "webdav_password"]) # Remove the password from the mediaflow proxy if user_data.mediaflow_config: - user_data.mediaflow_config.api_password = None + user_data.mediaflow_config.api_password = "••••••••" + configured_fields.append("mediaflow_api_password") + + # Check RPDB configuration + if user_data.rpdb_config: + user_data.rpdb_config.api_key = "••••••••" + configured_fields.append("rpdb_api_key") user_data.api_password = None @@ -223,6 +235,8 @@ async def configure( and not settings.is_public_instance, "kodi_code": kodi_code, "disabled_providers": settings.disabled_providers, + "configured_fields": configured_fields, + "secret_str": secret_str, }, ) @@ -620,8 +634,13 @@ async def get_streams( @app.post("/encrypt-user-data", tags=["user_data"]) +@app.post("/encrypt-user-data/{existing_secret_str}", tags=["user_data"]) @wrappers.rate_limit(30, 60 * 5, "user_data") -async def encrypt_user_data(user_data: schemas.UserData, request: Request): +async def encrypt_user_data( + user_data: schemas.UserData, + request: Request, + existing_secret_str: str | None = None, +): async def _validate_all_config() -> dict: if "p2p" in settings.disabled_providers and not user_data.streaming_provider: return { @@ -662,6 +681,48 @@ async def encrypt_user_data(user_data: schemas.UserData, request: Request): "message": f"Unexpected error during validation: {str(e)}", } + if existing_secret_str: + try: + existing_config = crypto.decrypt_user_data(existing_secret_str) + except ValueError: + existing_config = schemas.UserData() + + if user_data.streaming_provider and existing_config.streaming_provider: + if user_data.streaming_provider.password == "••••••••": + user_data.streaming_provider.password = ( + existing_config.streaming_provider.password + ) + if user_data.streaming_provider.token == "••••••••": + user_data.streaming_provider.token = ( + existing_config.streaming_provider.token + ) + + if user_data.streaming_provider.qbittorrent_config: + if ( + user_data.streaming_provider.qbittorrent_config.qbittorrent_password + == "••••••••" + ): + user_data.streaming_provider.qbittorrent_config.qbittorrent_password = ( + existing_config.streaming_provider.qbittorrent_config.qbittorrent_password + ) + if ( + user_data.streaming_provider.qbittorrent_config.webdav_password + == "••••••••" + ): + user_data.streaming_provider.qbittorrent_config.webdav_password = ( + existing_config.streaming_provider.qbittorrent_config.webdav_password + ) + + if user_data.mediaflow_config and existing_config.mediaflow_config: + if user_data.mediaflow_config.api_password == "••••••••": + user_data.mediaflow_config.api_password = ( + existing_config.mediaflow_config.api_password + ) + + if user_data.rpdb_config and existing_config.rpdb_config: + if user_data.rpdb_config.api_key == "••••••••": + user_data.rpdb_config.api_key = existing_config.rpdb_config.api_key + validation_result = await _validate_all_config() if validation_result["status"] == "error": return validation_result diff --git a/api/middleware.py b/api/middleware.py index 35793c7..4de4a39 100644 --- a/api/middleware.py +++ b/api/middleware.py @@ -49,6 +49,10 @@ class SecureLoggingMiddleware(BaseHTTPMiddleware): url_path = url_path.replace( request.path_params.get("secret_str"), "*MASKED*" ) + if request.path_params.get("existing_secret_str"): + url_path = url_path.replace( + request.path_params.get("existing_secret_str"), "*MASKED*" + ) logging.info( f'{ip} - "{request.method} {url_path} HTTP/1.1" {response.status_code} {process_time}' ) diff --git a/resources/css/styles.css b/resources/css/styles.css index 89c12d6..8881a94 100644 --- a/resources/css/styles.css +++ b/resources/css/styles.css @@ -479,6 +479,18 @@ input[type="text"]:focus, input[type="password"]:focus, select.form-control:focu margin-bottom: 0; } +.configured-field { + background-color: #f8f9fa; +} + +.reset-config-btn { + margin-left: 0.5rem; +} + +.input-group .reset-config-btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} .sort-item { background: rgba(var(--bs-primary-rgb), 0.05); diff --git a/resources/html/configure.html b/resources/html/configure.html index 5de6b92..8426954 100644 --- a/resources/html/configure.html +++ b/resources/html/configure.html @@ -237,10 +237,12 @@ + + + diff --git a/resources/js/config_script.js b/resources/js/config_script.js index 84fb0e1..3d82137 100644 --- a/resources/js/config_script.js +++ b/resources/js/config_script.js @@ -274,6 +274,7 @@ async function getInstallationUrl(isRedirect = false) { showLoadingWidget(); const userData = getUserData(); + const existingConfig = document.getElementById('existing_config').value; let urlPrefix = window.location.protocol + "//"; if (isRedirect) { urlPrefix = "stremio://"; @@ -284,8 +285,9 @@ async function getInstallationUrl(isRedirect = false) { showNotification('Validation failed. Please check your input.', 'error'); return null; } + const encryptUrl = '/encrypt-user-data' + (existingConfig ? `/${existingConfig}` : ''); - const response = await fetch('/encrypt-user-data', { + const response = await fetch(encryptUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' @@ -542,6 +544,52 @@ function setupPasswordToggle(passwordInputId, toggleButtonId, toggleIconId) { }); } +// Function to handle configured credential fields +function handleConfiguredFields(field, isConfigured = false) { + const inputField = document.getElementById(field); + const resetBtn = document.createElement('button'); + resetBtn.type = 'button'; + resetBtn.className = 'btn btn-outline-secondary reset-config-btn'; + resetBtn.innerHTML = ''; + resetBtn.title = 'Reset Configuration'; + + if (isConfigured) { + inputField.setAttribute('readonly', true); + inputField.classList.add('configured-field'); + + // Add reset button next to the field + if (!inputField.nextElementSibling?.classList.contains('reset-config-btn')) { + inputField.parentElement.appendChild(resetBtn); + } + + // Handle reset button click + resetBtn.onclick = () => { + inputField.value = ''; + inputField.removeAttribute('readonly'); + inputField.classList.remove('configured-field'); + resetBtn.remove(); + }; + } +} + +// Function to initialize configured fields +function initConfiguredFields(configuredFields) { + const sensitiveFields = [ + 'provider_token', + 'password', + 'qbittorrent_password', + 'webdav_password', + 'mediaflow_api_password', + 'rpdb_api_key' + ]; + + sensitiveFields.forEach(field => { + if (configuredFields.includes(field)) { + handleConfiguredFields(field, true); + } + }); +} + async function initiateKodiSetup() { // Show modal to input Kodi code const kodiCodeModal = new bootstrap.Modal(document.getElementById('kodiCodeModal')); @@ -700,12 +748,12 @@ document.addEventListener('DOMContentLoaded', function () { setupPasswordToggle('rpdb_api_key', 'toggleRPDBApiKey', 'toggleRPDBApiKeyIcon'); }); -document.addEventListener('DOMContentLoaded', function() { +document.addEventListener('DOMContentLoaded', function () { // Initialize sort direction toggles const toggleButtons = document.querySelectorAll('.sort-direction-toggle'); toggleButtons.forEach(button => { - button.addEventListener('click', function() { + button.addEventListener('click', function () { const sortId = this.dataset.sortId; const directionInput = document.getElementById(`direction_${sortId}`); const currentDirection = directionInput.value; @@ -732,7 +780,7 @@ document.addEventListener('DOMContentLoaded', function() { // Enable/disable sort direction buttons based on checkbox state const sortCheckboxes = document.querySelectorAll('[name="selected_sorting_options"]'); sortCheckboxes.forEach(checkbox => { - checkbox.addEventListener('change', function() { + checkbox.addEventListener('change', function () { const sortId = this.value; const toggleButton = document.querySelector(`[data-sort-id="${sortId}"]`); const sortItem = this.closest('.sort-item'); @@ -811,6 +859,10 @@ document.addEventListener('DOMContentLoaded', function () { }); document.addEventListener('DOMContentLoaded', function () { + // Initialize configured fields if they exist + const configuredFields = JSON.parse(document.getElementById('configured_fields')?.value || '[]'); + initConfiguredFields(configuredFields); + // Show or hide the language sort section based on the sorting options document.querySelectorAll('input[name="selected_sorting_options"]').forEach(checkbox => { checkbox.addEventListener('change', function () {