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.
This commit is contained in:
mhdzumair
2024-12-15 00:16:58 +05:30
parent 65c6eda6e5
commit fa425a00aa
5 changed files with 148 additions and 14 deletions
+67 -6
View File
@@ -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
+4
View File
@@ -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}'
)
+12
View File
@@ -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);
+9 -4
View File
@@ -237,10 +237,12 @@
<div id="token_input" class="mb-3" style="display:none">
<label for="provider_token" class="mb-3">Token: <span id="provider_token_tooltip" class="bi bi-question-circle" data-bs-toggle="tooltip" data-bs-placement="top"
title="Enter Encoded Token previously generated or Click 'Authorize' to generate a new token or Provide your Private Token."></span></label>
<input class="form-control" type="text" name="provider_token" id="provider_token" placeholder="Enter Token"
value="{{ user_data.streaming_provider.token if user_data.streaming_provider.token else '' }}">
<div class="invalid-feedback">
Token is required.
<div class="input-group">
<input class="form-control" type="text" name="provider_token" id="provider_token" placeholder="Enter Token"
value="{{ user_data.streaming_provider.token if user_data.streaming_provider.token else '' }}">
<div class="invalid-feedback">
Token is required.
</div>
</div>
</div>
@@ -894,6 +896,9 @@
</div>
</div>
<input type="hidden" id="configured_fields" value='{{ configured_fields|tojson|safe }}'>
<input type="hidden" id="existing_config" value="{{ secret_str if secret_str else '' }}">
<!-- JS for Bootstrap and form validation -->
<script src="/static/js/jquery-3.6.0.min.js"></script>
<script src="/static/js/popper.min.js"></script>
+56 -4
View File
@@ -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 = '<i class="bi bi-arrow-counterclockwise"></i>';
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 () {