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 () {