diff --git a/application/single_app/app.py b/application/single_app/app.py index 2354b1b5..805137d4 100644 --- a/application/single_app/app.py +++ b/application/single_app/app.py @@ -160,7 +160,7 @@ def configure_sessions(settings): redis_client = None try: if redis_auth_type == 'managed_identity': - print("Redis enabled using Managed Identity") + log_event("Redis enabled using Managed Identity", level=logging.INFO) from config import get_redis_cache_infrastructure_endpoint credential = DefaultAzureCredential() redis_hostname = redis_url.split('.')[0] @@ -175,9 +175,25 @@ def configure_sessions(settings): socket_connect_timeout=5, socket_timeout=5 ) + elif redis_auth_type == 'key_vault': + log_event("Redis enabled using Key Vault Secret", level=logging.INFO) + from functions_keyvault import retrieve_secret_direct + redis_key_secret_name = settings.get('redis_key', '').strip() + redis_password = retrieve_secret_direct(redis_key_secret_name) + if redis_password: + redis_password = redis_password.strip() + redis_client = Redis( + host=redis_url, + port=6380, + db=0, + password=redis_password, + ssl=True, + socket_connect_timeout=5, + socket_timeout=5 + ) else: redis_key = settings.get('redis_key', '').strip() - print("Redis enabled using Access Key") + log_event("Redis enabled using Access Key", level=logging.INFO) redis_client = Redis( host=redis_url, port=6380, @@ -190,7 +206,7 @@ def configure_sessions(settings): # Test the connection redis_client.ping() - print("✅ Redis connection successful") + log_event("✅ Redis connection successful", level=logging.INFO) app.config['SESSION_TYPE'] = 'redis' app.config['SESSION_REDIS'] = redis_client diff --git a/application/single_app/app_settings_cache.py b/application/single_app/app_settings_cache.py index cf908540..e7345efb 100644 --- a/application/single_app/app_settings_cache.py +++ b/application/single_app/app_settings_cache.py @@ -5,9 +5,14 @@ This supports the dynamic selection of redis or in-memory caching of settings. """ import json +import logging from redis import Redis from azure.identity import DefaultAzureCredential +# NOTE: functions_keyvault is imported locally inside configure_app_cache to avoid a circular +# import (functions_keyvault -> app_settings_cache -> functions_keyvault). +# functions_appinsights is also imported locally for the same reason. + _settings = None APP_SETTINGS_CACHE = {} update_settings_cache = None @@ -16,6 +21,8 @@ def configure_app_cache(settings, redis_cache_endpoint=None): global _settings, update_settings_cache, get_settings_cache, APP_SETTINGS_CACHE, app_cache_is_using_redis + # Local import to avoid circular dependency: functions_keyvault imports app_settings_cache. + from functions_appinsights import log_event _settings = settings use_redis = _settings.get('enable_redis_cache', False) @@ -24,9 +31,8 @@ def configure_app_cache(settings, redis_cache_endpoint=None): redis_url = settings.get('redis_url', '').strip() redis_auth_type = settings.get('redis_auth_type', 'key').strip().lower() if redis_auth_type == 'managed_identity': - print("[ASC] Redis enabled using Managed Identity") + log_event("[ASC] Redis enabled using Managed Identity", level=logging.INFO) credential = DefaultAzureCredential() - redis_hostname = redis_url.split('.')[0] cache_endpoint = redis_cache_endpoint token = credential.get_token(cache_endpoint) redis_client = Redis( @@ -36,9 +42,32 @@ def configure_app_cache(settings, redis_cache_endpoint=None): password=token.token, ssl=True ) + elif redis_auth_type == 'key_vault': + log_event("[ASC] Redis enabled using Key Vault Secret", level=logging.INFO) + # Local import to avoid circular dependency: functions_keyvault imports app_settings_cache. + from functions_keyvault import retrieve_secret_direct + redis_key_secret_name = settings.get('redis_key', '').strip() + try: + # Pass settings directly: get_settings_cache() is still None at this point + # because configure_app_cache has not finished initialising the cache yet. + redis_password = retrieve_secret_direct(redis_key_secret_name, settings=settings) + if redis_password: + redis_password = redis_password.strip() + log_event("[ASC] Redis key retrieved from Key Vault successfully", level=logging.INFO) + except Exception as kv_err: + log_event(f"[ASC] ERROR: Failed to retrieve Redis key from Key Vault: {kv_err}", level=logging.ERROR, exceptionTraceback=True) + raise + + redis_client = Redis( + host=redis_url, + port=6380, + db=0, + password=redis_password, + ssl=True + ) else: redis_key = settings.get('redis_key', '').strip() - print("[ASC] Redis enabled using Access Key") + log_event("[ASC] Redis enabled using Access Key", level=logging.INFO) redis_client = Redis( host=redis_url, port=6380, diff --git a/application/single_app/config.py b/application/single_app/config.py index da63c230..08c0adf1 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -94,7 +94,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.239.004" +VERSION = "0.239.005" SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key-change-in-production') diff --git a/application/single_app/functions_keyvault.py b/application/single_app/functions_keyvault.py index 2094814f..899c036c 100644 --- a/application/single_app/functions_keyvault.py +++ b/application/single_app/functions_keyvault.py @@ -66,10 +66,10 @@ def retrieve_secret_from_key_vault(secret_name, scope_value, scope="global", sou Exception: If retrieval fails or configuration is invalid. """ if source not in supported_sources: - logging.error(f"Source '{source}' is not supported. Supported sources: {supported_sources}") + log_event(f"Source '{source}' is not supported. Supported sources: {supported_sources}", level=logging.ERROR) raise ValueError(f"Source '{source}' is not supported. Supported sources: {supported_sources}") if scope not in supported_scopes: - logging.error(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") + log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level=logging.ERROR) raise ValueError(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") full_secret_name = build_full_secret_name(secret_name, scope_value, source, scope) @@ -104,12 +104,59 @@ def retrieve_secret_from_key_vault_by_full_name(full_secret_name): secret_client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential()) retrieved_secret = secret_client.get_secret(full_secret_name) - print(f"Secret '{full_secret_name}' retrieved successfully from Key Vault.") + log_event(f"Secret '{full_secret_name}' retrieved successfully from Key Vault.", level=logging.INFO) return retrieved_secret.value except Exception as e: - logging.error(f"Failed to retrieve secret '{full_secret_name}' from Key Vault: {str(e)}") + log_event(f"Failed to retrieve secret '{full_secret_name}' from Key Vault: {str(e)}", level=logging.ERROR, exceptionTraceback=True) return full_secret_name +def retrieve_secret_direct(secret_name, settings=None): + """ + Retrieve a secret directly from Key Vault by its exact name, bypassing source/scope name + validation and the enable_key_vault_secret_storage guard. Use this for infrastructure + secrets (e.g. Redis key) where the secret name is arbitrary and not controlled by the + scope_value--source--scope--secret_name convention. + + Args: + secret_name (str): The exact Key Vault secret name. + settings (dict, optional): Settings dict to use directly. If None, falls back to + app_settings_cache.get_settings_cache(). Pass settings explicitly when calling + before the cache is initialised (e.g. during configure_app_cache bootstrap). + + Returns: + str: The secret value. + + Raises: + ValueError: If Key Vault is not configured in settings. + Exception: If the secret cannot be retrieved. + """ + # Use provided settings directly when supplied (e.g. during bootstrap before the + # settings cache is initialised), otherwise fall back to the cache. + if settings is None: + settings = app_settings_cache.get_settings_cache() + + + enable_key_vault_secret_storage = settings.get("enable_key_vault_secret_storage", False) + + if not enable_key_vault_secret_storage: + raise ValueError("Key Vault secret storage is not enabled in settings.") + + key_vault_name = settings.get("key_vault_name", "").strip() + if not key_vault_name: + raise ValueError("Key Vault name is not configured in settings (key_vault_name).") + if not secret_name: + raise ValueError("secret_name must not be empty.") + + try: + key_vault_url = f"https://{key_vault_name}{KEY_VAULT_DOMAIN}" + # Pass settings through so get_keyvault_credential doesn't call the uninitialised cache. + secret_client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential(settings=settings)) + retrieved = secret_client.get_secret(secret_name) + log_event(f"Secret '{secret_name}' retrieved successfully from Key Vault.", level=logging.INFO) + return retrieved.value + except Exception as e: + log_event(f"Failed to retrieve secret '{secret_name}' from Key Vault: {str(e)}", level=logging.ERROR, exceptionTraceback=True) + raise def store_secret_in_key_vault(secret_name, secret_value, scope_value, source="global", scope="global"): """ @@ -130,32 +177,31 @@ def store_secret_in_key_vault(secret_name, secret_value, scope_value, source="gl settings = app_settings_cache.get_settings_cache() enable_key_vault_secret_storage = settings.get("enable_key_vault_secret_storage", False) if not enable_key_vault_secret_storage: - logging.warn(f"Key Vault secret storage is not enabled.") + log_event("Key Vault secret storage is not enabled.", level=logging.WARNING) return secret_value key_vault_name = settings.get("key_vault_name", None) if not key_vault_name: - logging.warn(f"Key Vault name is not configured.") + log_event("Key Vault name is not configured.", level=logging.WARNING) return secret_value if source not in supported_sources: - logging.error(f"Source '{source}' is not supported. Supported sources: {supported_sources}") + log_event(f"Source '{source}' is not supported. Supported sources: {supported_sources}", level=logging.ERROR) raise ValueError(f"Source '{source}' is not supported. Supported sources: {supported_sources}") if scope not in supported_scopes: - logging.error(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") + log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level=logging.ERROR) raise ValueError(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") - full_secret_name = build_full_secret_name(secret_name, scope_value, source, scope) try: key_vault_url = f"https://{key_vault_name}{KEY_VAULT_DOMAIN}" secret_client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential()) secret_client.set_secret(full_secret_name, secret_value) - print(f"Secret '{full_secret_name}' stored successfully in Key Vault.") + log_event(f"Secret '{full_secret_name}' stored successfully in Key Vault.", level=logging.INFO) return full_secret_name except Exception as e: - logging.error(f"Failed to store secret '{full_secret_name}' in Key Vault: {str(e)}") + log_event(f"Failed to store secret '{full_secret_name}' in Key Vault: {str(e)}", level=logging.ERROR, exceptionTraceback=True) return secret_value def build_full_secret_name(secret_name, scope_value, source, scope): @@ -175,7 +221,7 @@ def build_full_secret_name(secret_name, scope_value, source, scope): """ full_secret_name = f"{clean_name_for_keyvault(scope_value)}--{source}--{scope}--{clean_name_for_keyvault(secret_name)}" if not validate_secret_name_dynamic(full_secret_name): - logging.error(f"The full secret name '{full_secret_name}' is invalid.") + log_event(f"The full secret name '{full_secret_name}' is invalid.", level=logging.ERROR) raise ValueError(f"The full secret name '{full_secret_name}' is invalid.") return full_secret_name @@ -240,10 +286,10 @@ def keyvault_agent_save_helper(agent_dict, scope_value, scope="global"): full_secret_name = store_secret_in_key_vault(secret_name, value, scope_value, source=source, scope=scope) updated[key] = full_secret_name except Exception as e: - logging.error(f"Failed to store agent key '{key}' in Key Vault: {e}") + log_event(f"Failed to store agent key '{key}' in Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Failed to store agent key '{key}' in Key Vault: {e}") else: - log_event(f"Agent key '{key}' not found while APIM is '{use_apim}' or empty in agent '{agent_name}'. No action taken.", level="INFO") + log_event(f"Agent key '{key}' not found while APIM is '{use_apim}' or empty in agent '{agent_name}'. No action taken.", level=logging.INFO) return updated def keyvault_agent_get_helper(agent_dict, scope_value, scope="global", return_type=SecretReturnType.TRIGGER): @@ -283,7 +329,7 @@ def keyvault_agent_get_helper(agent_dict, scope_value, scope="global", return_ty else: updated[key] = ui_trigger_word except Exception as e: - logging.error(f"Failed to retrieve agent key '{key}' for agent '{agent_name}' from Key Vault: {e}") + log_event(f"Failed to retrieve agent key '{key}' for agent '{agent_name}' from Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) return updated return updated @@ -307,7 +353,7 @@ def keyvault_plugin_save_helper(plugin_dict, scope_value, scope="global"): This allows plugin writers to dynamically store secrets without custom code. """ if scope not in supported_scopes: - logging.error(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") + log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level=logging.ERROR) raise ValueError(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") source = "action" # Use 'action' for plugins per app convention updated = dict(plugin_dict) @@ -330,10 +376,10 @@ def keyvault_plugin_save_helper(plugin_dict, scope_value, scope="global"): new_auth['key'] = full_secret_name updated['auth'] = new_auth except Exception as e: - logging.error(f"Failed to store plugin key in Key Vault: {e}") + log_event(f"Failed to store plugin key in Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Failed to store plugin key in Key Vault: {e}") else: - print(f"Auth type '{auth_type}' does not require Key Vault storage. Does not match ") + log_event(f"Auth type '{auth_type}' does not require Key Vault storage for plugin '{plugin_name}'.", level=logging.INFO) # Handle additionalFields dynamic secrets additional_fields = updated.get('additionalFields', {}) @@ -356,7 +402,7 @@ def keyvault_plugin_save_helper(plugin_dict, scope_value, scope="global"): full_secret_name = store_secret_in_key_vault(akv_key, v, scope_value, source=addset_source, scope=scope) new_additional_fields[k] = full_secret_name except Exception as e: - logging.error(f"Failed to store plugin additionalField secret '{k}' in Key Vault: {e}") + log_event(f"Failed to store plugin additionalField secret '{k}' in Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Failed to store plugin additionalField secret '{k}' in Key Vault: {e}") updated['additionalFields'] = new_additional_fields return updated @@ -375,7 +421,7 @@ def keyvault_plugin_get_helper(plugin_dict, scope_value, scope="global", return_ dict: A new plugin dict with sensitive values replaced by ui_trigger_word if stored in Key Vault. """ if scope not in supported_scopes: - logging.error(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") + log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level=logging.ERROR) raise ValueError(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") updated = dict(plugin_dict) plugin_name = updated.get('name', 'plugin') @@ -399,7 +445,7 @@ def keyvault_plugin_get_helper(plugin_dict, scope_value, scope="global", return_ new_auth['key'] = ui_trigger_word updated['auth'] = new_auth except Exception as e: - logging.error(f"Failed to retrieve action {plugin_name} key from Key Vault: {e}") + log_event(f"Failed to retrieve action {plugin_name} key from Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Failed to retrieve action {plugin_name} key from Key Vault: {e}") additional_fields = updated.get('additionalFields', {}) @@ -419,7 +465,7 @@ def keyvault_plugin_get_helper(plugin_dict, scope_value, scope="global", return_ else: new_additional_fields[k] = ui_trigger_word except Exception as e: - logging.error(f"Failed to retrieve action additionalField secret '{k}' from Key Vault: {e}") + log_event(f"Failed to retrieve action additionalField secret '{k}' from Key Vault: {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Failed to retrieve action additionalField secret '{k}' from Key Vault: {e}") updated['additionalFields'] = new_additional_fields return updated @@ -439,13 +485,13 @@ def keyvault_plugin_delete_helper(plugin_dict, scope_value, scope="global"): Raises: """ if scope not in supported_scopes: - log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level="WARNING") + log_event(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}", level=logging.WARNING) raise ValueError(f"Scope '{scope}' is not supported. Supported scopes: {supported_scopes}") settings = app_settings_cache.get_settings_cache() enable_key_vault_secret_storage = settings.get("enable_key_vault_secret_storage", False) key_vault_name = settings.get("key_vault_name", None) if not enable_key_vault_secret_storage or not key_vault_name: - log_event(f"Key Vault secret storage is not enabled or key vault name is missing.", level="WARNING") + log_event("Key Vault secret storage is not enabled or key vault name is missing.", level=logging.WARNING) return plugin_dict source = "action" plugin_name = plugin_dict.get('name', 'plugin') @@ -456,11 +502,11 @@ def keyvault_plugin_delete_helper(plugin_dict, scope_value, scope="global"): if validate_secret_name_dynamic(secret_name): try: key_vault_url = f"https://{key_vault_name}{KEY_VAULT_DOMAIN}" - log_event(f"Deleting action secret '{secret_name}' for action '{plugin_name}' for '{scope}' '{scope_value}'", level="INFO") + log_event(f"Deleting action secret '{secret_name}' for action '{plugin_name}' for '{scope}' '{scope_value}'", level=logging.INFO) client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential()) client.begin_delete_secret(secret_name) except Exception as e: - logging.error(f"Error deleting action secret '{secret_name}' for action '{plugin_name}': {e}") + log_event(f"Error deleting action secret '{secret_name}' for action '{plugin_name}': {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Error deleting action secret '{secret_name}' for action '{plugin_name}': {e}") additional_fields = plugin_dict.get('additionalFields', {}) @@ -473,11 +519,11 @@ def keyvault_plugin_delete_helper(plugin_dict, scope_value, scope="global"): try: keyvault_secret_name = build_full_secret_name(akv_key, scope_value, addset_source, scope) key_vault_url = f"https://{key_vault_name}{KEY_VAULT_DOMAIN}" - log_event(f"Deleting action additionalField secret '{k}' for action '{plugin_name}' for '{scope}' '{scope_value}'", level="INFO") + log_event(f"Deleting action additionalField secret '{k}' for action '{plugin_name}' for '{scope}' '{scope_value}'", level=logging.INFO) client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential()) client.begin_delete_secret(keyvault_secret_name) except Exception as e: - logging.error(f"Error deleting action additionalField secret '{k}' for action '{plugin_name}': {e}") + log_event(f"Error deleting action additionalField secret '{k}' for action '{plugin_name}': {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Error deleting action additionalField secret '{k}' for action '{plugin_name}': {e}") return plugin_dict @@ -511,22 +557,29 @@ def keyvault_agent_delete_helper(agent_dict, scope_value, scope="global"): if validate_secret_name_dynamic(secret_name): try: key_vault_url = f"https://{key_vault_name}{KEY_VAULT_DOMAIN}" - log_event(f"Deleting agent secret '{secret_name}' for agent '{agent_name}' for '{scope}' '{scope_value}'", level="INFO") + log_event(f"Deleting agent secret '{secret_name}' for agent '{agent_name}' for '{scope}' '{scope_value}'", level=logging.INFO) client = SecretClient(vault_url=key_vault_url, credential=get_keyvault_credential()) client.begin_delete_secret(secret_name) except Exception as e: - logging.error(f"Error deleting secret '{secret_name}' for agent '{agent_name}': {e}") + log_event(f"Error deleting secret '{secret_name}' for agent '{agent_name}': {e}", level=logging.ERROR, exceptionTraceback=True) raise Exception(f"Error deleting secret '{secret_name}' for agent '{agent_name}': {e}") return agent_dict -def get_keyvault_credential(): +def get_keyvault_credential(settings=None): """ Get the Key Vault credential using DefaultAzureCredential, optionally with a managed identity client ID. + Args: + settings (dict, optional): Settings dict to use directly. If None, falls back to + app_settings_cache.get_settings_cache(). Pass settings explicitly when calling + before the cache is initialised (e.g. during configure_app_cache bootstrap). + Returns: DefaultAzureCredential: The credential object for Key Vault access. """ - settings = app_settings_cache.get_settings_cache() + if settings is None: + settings = app_settings_cache.get_settings_cache() + key_vault_identity = settings.get("key_vault_identity", None) if key_vault_identity is not None: credential = DefaultAzureCredential(managed_identity_client_id=key_vault_identity) diff --git a/application/single_app/route_backend_conversations.py b/application/single_app/route_backend_conversations.py index f267d729..ed15cb91 100644 --- a/application/single_app/route_backend_conversations.py +++ b/application/single_app/route_backend_conversations.py @@ -798,7 +798,7 @@ def get_conversation_metadata_api(conversation_id): "is_hidden": conversation_item.get('is_hidden', False), "scope_locked": conversation_item.get('scope_locked'), "locked_contexts": conversation_item.get('locked_contexts', []), - "chat_type": conversation_item.get('chat_type') + "chat_type": conversation_item.get('chat_type', 'personal') # Default to 'personal' if chat_type is not defined (legacy conversations) }), 200 except CosmosResourceNotFoundError: diff --git a/application/single_app/route_backend_settings.py b/application/single_app/route_backend_settings.py index 7be73134..aefb2f12 100644 --- a/application/single_app/route_backend_settings.py +++ b/application/single_app/route_backend_settings.py @@ -706,9 +706,18 @@ def _test_redis_connection(payload): cache_endpoint = get_redis_cache_infrastructure_endpoint(redis_hostname) token = credential.get_token(cache_endpoint) redis_password = token.token + elif redis_auth_type == 'key_vault': + if not redis_key: + return jsonify({'error': 'Key Vault secret name is required for Key Vault authentication'}), 400 + try: + from functions_keyvault import retrieve_secret_direct + redis_password = retrieve_secret_direct(redis_key) + except Exception as kv_err: + log_event(f"[REDIS_TEST] Key Vault retrieval failed for secret '{redis_key}': {str(kv_err)}", level="error") + return jsonify({'error': 'Failed to retrieve Redis key from Key Vault. Check Application Insights using "[REDIS_TEST]" for details.'}), 500 else: if not redis_key: - return jsonify({'error': 'Redis key is required for key auth'}), 400 + return jsonify({'error': 'Redis key is required for key authentication'}), 400 redis_password = redis_key r = redis.Redis( @@ -1043,4 +1052,4 @@ def _test_key_vault_connection(payload): except Exception as e: log_event(f"[AKV_TEST] Key Vault connection error: {str(e)}", level="error") - return jsonify({'error': f'Key Vault connection error. Check Application Insights using "[AKV_TEST]" for details.'}), 500 \ No newline at end of file + return jsonify({'error': 'Key Vault connection failed. Check Application Insights using "[AKV_TEST]" for details.'}), 500 \ No newline at end of file diff --git a/application/single_app/static/css/chats.css b/application/single_app/static/css/chats.css index 38e11c3a..f672b28f 100644 --- a/application/single_app/static/css/chats.css +++ b/application/single_app/static/css/chats.css @@ -1046,7 +1046,7 @@ a.citation-link:hover { .message-content { display: flex; align-items: flex-end; - overflow: visible; /* Allow dropdown menus to appear outside content */ + overflow: auto; /* Preserving higher level visible property while allowing response message scroll if needed */ } .message-content.flex-row-reverse { diff --git a/application/single_app/static/css/styles.css b/application/single_app/static/css/styles.css index e537590d..c0632a99 100644 --- a/application/single_app/static/css/styles.css +++ b/application/single_app/static/css/styles.css @@ -854,3 +854,145 @@ main { [data-bs-theme="dark"] .message-content a:visited { color: #b399ff !important; /* Purple-ish for visited links */ } + +/* ============================================= + SimpleMDE Prompt Content Toolbar - Bootstrap Icons replacement for Font Awesome + ============================================= */ +.editor-toolbar a[title] { + font-family: "bootstrap-icons" !important; + font-style: normal; + font-size: 1rem; + line-height: 30px; +} + +.editor-toolbar a.fa-bold::before { content: "\f5f0"; } /* bi-type-bold */ +.editor-toolbar a.fa-italic::before { content: "\f5f4"; } /* bi-type-italic */ +.editor-toolbar a.fa-strikethrough::before { content: "\f5f5"; } /* bi-type-strikethrough */ +.editor-toolbar a.fa-header::before { content: "\f5f1"; } /* bi-type-h1 */ +.editor-toolbar a.fa-quote-left::before { content: "\f190"; } /* bi-blockquote-left */ +.editor-toolbar a.fa-list-ul::before { content: "\f478"; } /* bi-list-ul */ +.editor-toolbar a.fa-list-ol::before { content: "\f475"; } /* bi-list-ol */ +.editor-toolbar a.fa-code::before { content: "\f2c6"; } /* bi-code-slash */ +.editor-toolbar a.fa-link::before { content: "\f470"; } /* bi-link-45deg */ +.editor-toolbar a.fa-picture-o::before { content: "\f42a"; } /* bi-image */ +.editor-toolbar a.fa-table::before { content: "\f5aa"; } /* bi-table */ +.editor-toolbar a.fa-minus::before { content: "\f63b"; } /* bi-dash-lg */ +.editor-toolbar a.fa-eye::before { content: "\f341"; } /* bi-eye */ +.editor-toolbar a.fa-columns::before { content: "\f460"; } /* bi-layout-split */ +.editor-toolbar a.fa-arrows-alt::before { content: "\f14d"; } /* bi-arrows-fullscreen */ +.editor-toolbar a.fa-undo::before { content: "\f117"; } /* bi-arrow-counterclockwise */ +.editor-toolbar a.fa-repeat::before { content: "\f116"; } /* bi-arrow-clockwise */ +.editor-toolbar a.fa-question-circle::before { content: "\f505"; } /* bi-question-circle */ +.editor-toolbar a.fa-eraser::before { content: "\f331"; } /* bi-eraser */ + +.editor-toolbar a::before { + font-family: "bootstrap-icons" !important; + font-style: normal; + font-weight: normal; + display: inline-block; +} + +.editor-toolbar { + opacity: 1 !important; +} + +.editor-toolbar a { + font-size: 0; + width: 30px; + height: 30px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.editor-toolbar a::before { + font-size: 1rem; +} + +/* ============================================= + SimpleMDE Dark Mode Overrides + ============================================= */ +[data-bs-theme="dark"] .editor-toolbar { + background-color: #343a40; + border-color: #495057; +} + +[data-bs-theme="dark"] .editor-toolbar a { + color: #adb5bd !important; +} + +[data-bs-theme="dark"] .editor-toolbar a::before { + color: #adb5bd; +} + +[data-bs-theme="dark"] .editor-toolbar a:hover, +[data-bs-theme="dark"] .editor-toolbar a:focus { + background-color: #495057; + border-color: #6c757d; + color: #e9ecef !important; +} + +[data-bs-theme="dark"] .editor-toolbar a:hover::before, +[data-bs-theme="dark"] .editor-toolbar a:focus::before { + color: #e9ecef; +} + +[data-bs-theme="dark"] .editor-toolbar a.active, +[data-bs-theme="dark"] .editor-toolbar a.active::before { + color: #86b7fe !important; + background-color: #1e3a5f; +} + +[data-bs-theme="dark"] .editor-toolbar i.separator { + border-left-color: #495057; + border-right-color: #495057; +} + +[data-bs-theme="dark"] .CodeMirror { + background-color: #212529; + color: #e9ecef; + border-color: #495057; +} + +[data-bs-theme="dark"] .CodeMirror-cursor { + border-left-color: #e9ecef; +} + +[data-bs-theme="dark"] .CodeMirror .CodeMirror-selected { + background-color: #1e3a5f !important; +} + +[data-bs-theme="dark"] .CodeMirror-focused .CodeMirror-selected { + background-color: #0d6efd !important; +} + +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line::selection, +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line > span::selection, +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line > span > span::selection { + background-color: #0d6efd !important; + color: #ffffff !important; +} + +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line::-moz-selection, +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line > span::-moz-selection, +[data-bs-theme="dark"] .CodeMirror .CodeMirror-line > span > span::-moz-selection { + background-color: #0d6efd !important; + color: #ffffff !important; +} + +[data-bs-theme="dark"] .editor-preview { + background-color: #2b3035; + color: #e9ecef; +} + +[data-bs-theme="dark"] .editor-preview-side { + background-color: #2b3035; + color: #e9ecef; + border-left-color: #495057; +} + +[data-bs-theme="dark"] .editor-statusbar { + background-color: #343a40; + color: #adb5bd; + border-top-color: #495057; +} \ No newline at end of file diff --git a/application/single_app/static/js/admin/admin_settings.js b/application/single_app/static/js/admin/admin_settings.js index 85719128..c6bdec36 100644 --- a/application/single_app/static/js/admin/admin_settings.js +++ b/application/single_app/static/js/admin/admin_settings.js @@ -1867,14 +1867,30 @@ function setupToggles() { const redisAuthType = document.getElementById('redis_auth_type'); if (redisAuthType) { const redisKeyContainer = document.getElementById('redis_key_container'); + const redisKeyLabel = document.getElementById('redis_key_label'); + + // Helper to update the label text based on auth type + function updateRedisKeyLabel(authTypeValue) { + if (!redisKeyLabel) return; + redisKeyLabel.textContent = authTypeValue === 'key_vault' ? 'Key Vault Secret Name' : 'Redis Access Key'; + } + // Set initial state on load if (redisKeyContainer) { - redisKeyContainer.style.display = (redisAuthType.value === 'key') ? 'block' : 'none'; + redisKeyContainer.classList.toggle('d-none', !(redisAuthType.value === 'key' || redisAuthType.value === 'key_vault')); } + updateRedisKeyLabel(redisAuthType.value); + redisAuthType.addEventListener('change', function () { if (redisKeyContainer) { - redisKeyContainer.style.display = (this.value === 'key') ? 'block' : 'none'; + redisKeyContainer.classList.toggle('d-none', !(this.value === 'key' || this.value === 'key_vault')); + } + const redisKeyVaultHint = document.getElementById('redis_key_vault_hint'); + if (redisKeyVaultHint) { + redisKeyVaultHint.classList.toggle('d-none', this.value !== 'key_vault'); } + updateRedisKeyLabel(this.value); + markFormAsModified(); }); } @@ -2179,7 +2195,8 @@ function setupTestButtons() { const payload = { test_type: 'redis', endpoint: document.getElementById('redis_url').value, - key: document.getElementById('redis_key').value + key: document.getElementById('redis_key').value, + auth_type: document.getElementById('redis_auth_type').value }; try { diff --git a/application/single_app/templates/admin_settings.html b/application/single_app/templates/admin_settings.html index 7d01f7da..9c385b0e 100644 --- a/application/single_app/templates/admin_settings.html +++ b/application/single_app/templates/admin_settings.html @@ -2036,14 +2036,26 @@