style: apply ruff auto-fix

- Auto-formatted code with ruff format
- Applied ruff linting fixes with --fix

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
github-actions[bot]
2026-02-23 09:33:48 +00:00
parent 7fe25474bb
commit 540541eec6
13 changed files with 193 additions and 550 deletions
+14 -48
View File
@@ -49,9 +49,7 @@ async def exchange_dropbox_token(
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="Dropbox", token_url=token_url, payload=payload
)
token_data = exchange_oauth_token(provider_name="Dropbox", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
@@ -79,19 +77,13 @@ async def update_dropbox_settings(
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Update settings in memory and persist to database
if refresh_token:
settings.dropbox_refresh_token = refresh_token
save_setting_to_db(
db, "dropbox_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "dropbox_refresh_token", refresh_token, changed_by=changed_by)
logger.info("Updated DROPBOX_REFRESH_TOKEN in memory and database")
if app_key:
@@ -101,9 +93,7 @@ async def update_dropbox_settings(
if app_secret:
settings.dropbox_app_secret = app_secret
save_setting_to_db(
db, "dropbox_app_secret", app_secret, changed_by=changed_by
)
save_setting_to_db(db, "dropbox_app_secret", app_secret, changed_by=changed_by)
logger.info("Updated DROPBOX_APP_SECRET in memory and database")
if folder_path:
@@ -135,11 +125,7 @@ async def test_dropbox_token(request: Request):
try:
logger.info("Testing Dropbox token validity")
if (
not settings.dropbox_refresh_token
or not settings.dropbox_app_key
or not settings.dropbox_app_secret
):
if not settings.dropbox_refresh_token or not settings.dropbox_app_key or not settings.dropbox_app_secret:
logger.warning("Dropbox credentials not fully configured")
return {
"status": "error",
@@ -167,14 +153,10 @@ async def test_dropbox_token(request: Request):
"client_secret": settings.dropbox_app_secret,
}
refresh_response = requests.post(
refresh_url, data=refresh_data, timeout=settings.http_request_timeout
)
refresh_response = requests.post(refresh_url, data=refresh_data, timeout=settings.http_request_timeout)
if refresh_response.status_code != 200:
logger.error(
f"Failed to refresh Dropbox token: {refresh_response.text}"
)
logger.error(f"Failed to refresh Dropbox token: {refresh_response.text}")
return {
"status": "error",
"message": "Refresh token has expired or is invalid",
@@ -193,9 +175,7 @@ async def test_dropbox_token(request: Request):
)
if response.status_code != 200:
logger.error(
f"Dropbox token test failed: {response.status_code} {response.text}"
)
logger.error(f"Dropbox token test failed: {response.status_code} {response.text}")
return {
"status": "error",
"message": f"Token validation failed with status {response.status_code}: {response.text}",
@@ -243,11 +223,7 @@ async def save_dropbox_settings(
try:
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Update settings in memory
@@ -262,27 +238,19 @@ async def save_dropbox_settings(
# Persist to database (primary storage)
if refresh_token:
save_setting_to_db(
db, "dropbox_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "dropbox_refresh_token", refresh_token, changed_by=changed_by)
if app_key:
save_setting_to_db(db, "dropbox_app_key", app_key, changed_by=changed_by)
if app_secret:
save_setting_to_db(
db, "dropbox_app_secret", app_secret, changed_by=changed_by
)
save_setting_to_db(db, "dropbox_app_secret", app_secret, changed_by=changed_by)
if folder_path:
save_setting_to_db(db, "dropbox_folder", folder_path, changed_by=changed_by)
# Best-effort .env file write
try:
env_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
)
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if not os.path.exists(env_path):
logger.warning(
f".env file not found at {env_path}, skipping file write"
)
logger.warning(f".env file not found at {env_path}, skipping file write")
else:
logger.info(f"Updating Dropbox settings in {env_path}")
@@ -303,9 +271,7 @@ async def save_dropbox_settings(
stripped_line = line.rstrip()
is_updated = False
for key, value in dropbox_settings.items():
if stripped_line.startswith(
f"{key}="
) or stripped_line.startswith(f"# {key}="):
if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="):
new_env_lines.append(f"{key}={value}")
updated.add(key)
is_updated = True
+18 -57
View File
@@ -49,9 +49,7 @@ async def exchange_google_drive_token(
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="Google Drive", token_url=token_url, payload=payload
)
token_data = exchange_oauth_token(provider_name="Google Drive", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
@@ -80,11 +78,7 @@ async def update_google_drive_settings(
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Convert use_oauth string to boolean
@@ -93,30 +87,22 @@ async def update_google_drive_settings(
# Update settings in memory and persist to database
if refresh_token:
settings.google_drive_refresh_token = refresh_token
save_setting_to_db(
db, "google_drive_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_refresh_token", refresh_token, changed_by=changed_by)
logger.info("Updated GOOGLE_DRIVE_REFRESH_TOKEN in memory and database")
if client_id:
settings.google_drive_client_id = client_id
save_setting_to_db(
db, "google_drive_client_id", client_id, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_client_id", client_id, changed_by=changed_by)
logger.info("Updated GOOGLE_DRIVE_CLIENT_ID in memory and database")
if client_secret:
settings.google_drive_client_secret = client_secret
save_setting_to_db(
db, "google_drive_client_secret", client_secret, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_client_secret", client_secret, changed_by=changed_by)
logger.info("Updated GOOGLE_DRIVE_CLIENT_SECRET in memory and database")
if folder_id:
settings.google_drive_folder_id = folder_id
save_setting_to_db(
db, "google_drive_folder_id", folder_id, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_folder_id", folder_id, changed_by=changed_by)
logger.info("Updated GOOGLE_DRIVE_FOLDER_ID in memory and database")
# Set the OAuth flag
@@ -127,9 +113,7 @@ async def update_google_drive_settings(
str(use_oauth_bool).lower(),
changed_by=changed_by,
)
logger.info(
f"Updated GOOGLE_DRIVE_USE_OAUTH in memory and database to {use_oauth_bool}"
)
logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory and database to {use_oauth_bool}")
notify_settings_updated()
@@ -154,8 +138,7 @@ async def test_google_drive_token(request: Request):
Tests both OAuth and service account approaches based on configuration.
"""
try:
from app.tasks.upload_to_google_drive import (get_drive_service_oauth,
get_google_drive_service)
from app.tasks.upload_to_google_drive import get_drive_service_oauth, get_google_drive_service
logger.info("Testing Google Drive token validity")
@@ -232,9 +215,7 @@ async def test_google_drive_token(request: Request):
else:
# Test service account connection
if not settings.google_drive_credentials_json:
logger.warning(
"Google Drive service account credentials not configured"
)
logger.warning("Google Drive service account credentials not configured")
return {
"status": "error",
"message": "Google Drive service account credentials are not configured",
@@ -253,9 +234,7 @@ async def test_google_drive_token(request: Request):
else:
user_display = user_email
logger.info(
f"Successfully connected to Google Drive using service account as {user_display}"
)
logger.info(f"Successfully connected to Google Drive using service account as {user_display}")
return {
"status": "success",
@@ -398,20 +377,14 @@ async def save_dropbox_settings(
"""
try:
# Get the path to the .env file
env_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
)
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
# Convert use_oauth string to boolean
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Define settings to update
@@ -446,9 +419,7 @@ async def save_dropbox_settings(
stripped_line = line.rstrip()
is_updated = False
for key, value in drive_settings.items():
if stripped_line.startswith(
f"{key}="
) or stripped_line.startswith(f"# {key}="):
if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="):
# Uncomment if commented out - check the original stripped line
new_env_lines.append(f"{key}={value}")
updated.add(key)
@@ -468,9 +439,7 @@ async def save_dropbox_settings(
logger.info("Successfully updated Google Drive settings in .env file")
except Exception as e:
logger.warning(
f"Failed to update .env file: {str(e)}, but will continue with in-memory update"
)
logger.warning(f"Failed to update .env file: {str(e)}, but will continue with in-memory update")
else:
logger.warning(
f".env file not found at {env_path}, skipping file update but continuing with in-memory update"
@@ -497,21 +466,13 @@ async def save_dropbox_settings(
changed_by=changed_by,
)
if refresh_token:
save_setting_to_db(
db, "google_drive_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_refresh_token", refresh_token, changed_by=changed_by)
if client_id:
save_setting_to_db(
db, "google_drive_client_id", client_id, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_client_id", client_id, changed_by=changed_by)
if client_secret:
save_setting_to_db(
db, "google_drive_client_secret", client_secret, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_client_secret", client_secret, changed_by=changed_by)
if folder_id:
save_setting_to_db(
db, "google_drive_folder_id", folder_id, changed_by=changed_by
)
save_setting_to_db(db, "google_drive_folder_id", folder_id, changed_by=changed_by)
notify_settings_updated()
+25 -79
View File
@@ -51,9 +51,7 @@ async def exchange_onedrive_token(
}
# Use shared OAuth helper (handles secure logging and error handling)
token_data = exchange_oauth_token(
provider_name="OneDrive", token_url=token_url, payload=payload
)
token_data = exchange_oauth_token(provider_name="OneDrive", token_url=token_url, payload=payload)
# Return just what's needed by the frontend
return {
@@ -94,9 +92,7 @@ async def test_onedrive_token(request: Request):
"scope": "offline_access Files.ReadWrite",
}
response = requests.post(
token_url, data=refresh_data, timeout=settings.http_request_timeout
)
response = requests.post(token_url, data=refresh_data, timeout=settings.http_request_timeout)
if response.status_code != 200:
logger.error(f"Failed to refresh OneDrive token: {response.text}")
@@ -108,25 +104,19 @@ async def test_onedrive_token(request: Request):
token_data = response.json()
access_token = token_data.get("access_token")
expires_in = token_data.get(
"expires_in", 3600
) # Default to 1 hour if not specified
expires_in = token_data.get("expires_in", 3600) # Default to 1 hour if not specified
# Check if we got a new refresh token (Microsoft sometimes issues a new one)
new_refresh_token = token_data.get("refresh_token")
if new_refresh_token and new_refresh_token != settings.onedrive_refresh_token:
logger.info(
"Received new refresh token from Microsoft - will update configuration"
)
logger.info("Received new refresh token from Microsoft - will update configuration")
# Update refresh token in memory
settings.onedrive_refresh_token = new_refresh_token
# Also try to update .env file if it exists
try:
env_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
)
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if os.path.exists(env_path):
with open(env_path, "r") as f:
env_lines = f.readlines()
@@ -136,17 +126,13 @@ async def test_onedrive_token(request: Request):
for line in env_lines:
if line.startswith("ONEDRIVE_REFRESH_TOKEN="):
updated_lines.append(
f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n"
)
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
updated = True
else:
updated_lines.append(line)
if not updated:
updated_lines.append(
f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n"
)
updated_lines.append(f"ONEDRIVE_REFRESH_TOKEN={new_refresh_token}\n")
with open(env_path, "w") as f:
f.writelines(updated_lines)
@@ -172,22 +158,16 @@ async def test_onedrive_token(request: Request):
finally:
_db.close()
except Exception as _e:
logger.warning(
f"Failed to persist rotated OneDrive refresh token to database: {_e}"
)
logger.warning(f"Failed to persist rotated OneDrive refresh token to database: {_e}")
# Test the access token by getting user information
user_info_url = "https://graph.microsoft.com/v1.0/me"
headers = {"Authorization": f"Bearer {access_token}"}
user_response = requests.get(
user_info_url, headers=headers, timeout=settings.http_request_timeout
)
user_response = requests.get(user_info_url, headers=headers, timeout=settings.http_request_timeout)
if user_response.status_code != 200:
logger.error(
f"OneDrive token test failed: {user_response.status_code} {user_response.text}"
)
logger.error(f"OneDrive token test failed: {user_response.status_code} {user_response.text}")
return {
"status": "error",
"message": f"Token validation failed with status {user_response.status_code}: {user_response.text}",
@@ -263,22 +243,14 @@ async def save_onedrive_settings(
try:
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Best-effort .env file write
try:
env_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env"
)
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
if not os.path.exists(env_path):
logger.warning(
f".env file not found at {env_path}, skipping file write"
)
logger.warning(f".env file not found at {env_path}, skipping file write")
else:
logger.info(f"Updating OneDrive settings in {env_path}")
@@ -301,9 +273,7 @@ async def save_onedrive_settings(
stripped_line = line.rstrip()
is_updated = False
for key, value in onedrive_settings.items():
if stripped_line.startswith(
f"{key}="
) or stripped_line.startswith(f"# {key}="):
if stripped_line.startswith(f"{key}=") or stripped_line.startswith(f"# {key}="):
new_env_lines.append(f"{key}={value}")
updated.add(key)
is_updated = True
@@ -336,25 +306,15 @@ async def save_onedrive_settings(
# Persist to database (primary)
if refresh_token:
save_setting_to_db(
db, "onedrive_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_refresh_token", refresh_token, changed_by=changed_by)
if client_id:
save_setting_to_db(
db, "onedrive_client_id", client_id, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_client_id", client_id, changed_by=changed_by)
if client_secret:
save_setting_to_db(
db, "onedrive_client_secret", client_secret, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_client_secret", client_secret, changed_by=changed_by)
if tenant_id:
save_setting_to_db(
db, "onedrive_tenant_id", tenant_id, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_tenant_id", tenant_id, changed_by=changed_by)
if folder_path:
save_setting_to_db(
db, "onedrive_folder_path", folder_path, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_folder_path", folder_path, changed_by=changed_by)
notify_settings_updated()
@@ -388,47 +348,33 @@ async def update_onedrive_settings(
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "wizard"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "wizard"
)
# Update settings in memory and persist to database
if refresh_token:
settings.onedrive_refresh_token = refresh_token
save_setting_to_db(
db, "onedrive_refresh_token", refresh_token, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_refresh_token", refresh_token, changed_by=changed_by)
logger.info("Updated ONEDRIVE_REFRESH_TOKEN in memory and database")
if client_id:
settings.onedrive_client_id = client_id
save_setting_to_db(
db, "onedrive_client_id", client_id, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_client_id", client_id, changed_by=changed_by)
logger.info("Updated ONEDRIVE_CLIENT_ID in memory and database")
if client_secret:
settings.onedrive_client_secret = client_secret
save_setting_to_db(
db, "onedrive_client_secret", client_secret, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_client_secret", client_secret, changed_by=changed_by)
logger.info("Updated ONEDRIVE_CLIENT_SECRET in memory and database")
if tenant_id:
settings.onedrive_tenant_id = tenant_id
save_setting_to_db(
db, "onedrive_tenant_id", tenant_id, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_tenant_id", tenant_id, changed_by=changed_by)
logger.info("Updated ONEDRIVE_TENANT_ID in memory and database")
if folder_path:
settings.onedrive_folder_path = folder_path
save_setting_to_db(
db, "onedrive_folder_path", folder_path, changed_by=changed_by
)
save_setting_to_db(db, "onedrive_folder_path", folder_path, changed_by=changed_by)
logger.info("Updated ONEDRIVE_FOLDER_PATH in memory and database")
notify_settings_updated()
+28 -64
View File
@@ -11,16 +11,19 @@ from sqlalchemy.orm import Session
from app.config import settings
from app.database import get_db
from app.utils.input_validation import (validate_setting_key,
validate_setting_key_format)
from app.utils.settings_service import (SETTING_METADATA,
delete_setting_from_db,
get_all_settings_from_db,
get_audit_log, get_setting_history,
get_setting_metadata,
get_settings_by_category,
rollback_setting, save_setting_to_db,
validate_setting_value)
from app.utils.input_validation import validate_setting_key, validate_setting_key_format
from app.utils.settings_service import (
SETTING_METADATA,
delete_setting_from_db,
get_all_settings_from_db,
get_audit_log,
get_setting_history,
get_setting_metadata,
get_settings_by_category,
rollback_setting,
save_setting_to_db,
validate_setting_value,
)
from app.utils.settings_sync import notify_settings_updated
logger = logging.getLogger(__name__)
@@ -37,9 +40,7 @@ def require_admin(request: Request) -> dict:
"""
user = request.session.get("user")
if not user or not user.get("is_admin"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required"
)
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
return user
@@ -93,9 +94,7 @@ async def get_settings(request: Request, db: DbSession, admin: AdminUser):
# Get settings organized by category
categories = get_settings_by_category()
return SettingsListResponse(
settings=current_settings, categories=categories, db_settings=db_settings
)
return SettingsListResponse(settings=current_settings, categories=categories, db_settings=db_settings)
except Exception as e:
logger.error(f"Error retrieving settings: {e}")
raise HTTPException(
@@ -118,9 +117,7 @@ async def get_setting(key: str, request: Request, db: DbSession, admin: AdminUse
# Get metadata
metadata = get_setting_metadata(key)
return SettingResponse(
key=key, value=str(value) if value is not None else None, metadata=metadata
)
return SettingResponse(key=key, value=str(value) if value is not None else None, metadata=metadata)
except Exception as e:
logger.error(f"Error retrieving setting {key}: {e}")
raise HTTPException(
@@ -147,18 +144,12 @@ async def update_setting(
if setting.value is not None:
is_valid, error_message = validate_setting_value(key, setting.value)
if not is_valid:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail=error_message
)
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_message)
# Determine the username for the audit log
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "admin"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin"
)
# Save to database
@@ -203,11 +194,7 @@ async def delete_setting(key: str, request: Request, db: DbSession, admin: Admin
try:
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "admin"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin"
)
success = delete_setting_from_db(db, key, changed_by=changed_by)
@@ -291,9 +278,7 @@ async def list_credentials(request: Request, db: DbSession, admin: AdminUser):
@router.post("/bulk-update")
async def bulk_update_settings(
updates: list[SettingUpdate], request: Request, db: DbSession, admin: AdminUser
):
async def bulk_update_settings(updates: list[SettingUpdate], request: Request, db: DbSession, admin: AdminUser):
"""
Update multiple settings at once.
Admin only.
@@ -303,36 +288,24 @@ async def bulk_update_settings(
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "admin"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin"
)
for update in updates:
try:
# Validate the setting value
if update.value is not None:
is_valid, error_message = validate_setting_value(
update.key, update.value
)
is_valid, error_message = validate_setting_value(update.key, update.value)
if not is_valid:
errors.append({"key": update.key, "error": error_message})
continue
# Save to database
success = save_setting_to_db(
db, update.key, update.value, changed_by=changed_by
)
success = save_setting_to_db(db, update.key, update.value, changed_by=changed_by)
if success:
results.append(
{"key": update.key, "value": update.value, "status": "success"}
)
results.append({"key": update.key, "value": update.value, "status": "success"})
else:
errors.append(
{"key": update.key, "error": "Failed to save to database"}
)
errors.append({"key": update.key, "error": "Failed to save to database"})
except Exception as e:
logger.error(f"Error updating setting {update.key}: {e}")
errors.append({"key": update.key, "error": str(e)})
@@ -340,10 +313,7 @@ async def bulk_update_settings(
if results:
notify_settings_updated()
restart_required = any(
get_setting_metadata(result["key"]).get("restart_required", False)
for result in results
)
restart_required = any(get_setting_metadata(result["key"]).get("restart_required", False) for result in results)
return {
"success": len(errors) == 0,
@@ -422,11 +392,7 @@ async def rollback_setting_to_history(
try:
user = request.session.get("user", {}) if hasattr(request, "session") else {}
changed_by = (
user.get("preferred_username")
or user.get("username")
or user.get("email")
or user.get("id")
or "admin"
user.get("preferred_username") or user.get("username") or user.get("email") or user.get("id") or "admin"
)
success = rollback_setting(db, key, history_id, changed_by=changed_by)
@@ -498,9 +464,7 @@ async def export_env_settings(
return FastAPIResponse(
content=content,
media_type="text/plain",
headers={
"Content-Disposition": f'attachment; filename="docuelevate-{source}.env"'
},
headers={"Content-Disposition": f'attachment; filename="docuelevate-{source}.env"'},
)
except Exception as e:
logger.error(f"Error exporting settings: {e}")
+10 -13
View File
@@ -3,32 +3,30 @@
from celery.schedules import crontab
# Ensure tasks are loaded
from app import \
tasks # noqa: F401 - Imports app/tasks.py so Celery can register tasks
from app import tasks # noqa: F401 - Imports app/tasks.py so Celery can register tasks
# Import the shared Celery instance
from app.celery_app import celery
from app.config import settings
from app.tasks.check_credentials import check_credentials
from app.tasks.convert_to_pdf import convert_to_pdf # noqa: F401
from app.tasks.embed_metadata_into_pdf import \
embed_metadata_into_pdf # noqa: F401
from app.tasks.extract_metadata_with_gpt import \
extract_metadata_with_gpt # noqa: F401
from app.tasks.embed_metadata_into_pdf import embed_metadata_into_pdf # noqa: F401
from app.tasks.extract_metadata_with_gpt import extract_metadata_with_gpt # noqa: F401
from app.tasks.imap_tasks import pull_all_inboxes # noqa: F401
from app.tasks.monitor_stalled_steps import monitor_stalled_steps # noqa: F401
# **Ensure all tasks are imported before Celery starts**
from app.tasks.process_document import process_document # noqa: F401
from app.tasks.process_with_azure_document_intelligence import \
process_with_azure_document_intelligence # noqa: F401
from app.tasks.process_with_azure_document_intelligence import process_with_azure_document_intelligence # noqa: F401
from app.tasks.refine_text_with_gpt import refine_text_with_gpt # noqa: F401
from app.tasks.rotate_pdf_pages import rotate_pdf_pages # noqa: F401
from app.tasks.send_to_all import send_to_all_destinations # noqa: F401
# Import new send tasks
from app.tasks.upload_to_dropbox import upload_to_dropbox # noqa: F401
from app.tasks.upload_to_email import upload_to_email # noqa: F401
from app.tasks.upload_to_ftp import upload_to_ftp # noqa: F401
from app.tasks.upload_to_google_drive import \
upload_to_google_drive # noqa: F401
from app.tasks.upload_to_google_drive import upload_to_google_drive # noqa: F401
from app.tasks.upload_to_nextcloud import upload_to_nextcloud # noqa: F401
from app.tasks.upload_to_onedrive import upload_to_onedrive # noqa: F401
from app.tasks.upload_to_paperless import upload_to_paperless # noqa: F401
@@ -36,6 +34,7 @@ from app.tasks.upload_to_s3 import upload_to_s3 # noqa: F401
from app.tasks.upload_to_sftp import upload_to_sftp # noqa: F401
from app.tasks.upload_to_webdav import upload_to_webdav # noqa: F401
from app.tasks.uptime_kuma_tasks import ping_uptime_kuma # noqa: F401
# Register the settings reload signal handler so workers pick up config changes
from app.utils.settings_sync import register_settings_reload_signal
@@ -95,6 +94,4 @@ celery.conf.beat_schedule = {
}
# Remove None entries from beat_schedule
celery.conf.beat_schedule = {
k: v for k, v in celery.conf.beat_schedule.items() if v is not None
}
celery.conf.beat_schedule = {k: v for k, v in celery.conf.beat_schedule.items() if v is not None}
+11 -32
View File
@@ -1,7 +1,6 @@
# app/models.py
from sqlalchemy import (Boolean, Column, DateTime, ForeignKey, Integer, String,
Text, UniqueConstraint, func)
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, func
from app.database import Base
@@ -70,33 +69,21 @@ class FileProcessingStep(Base):
id = Column(Integer, primary_key=True, index=True)
file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=False, index=True)
step_name = Column(
String, nullable=False, index=True
) # e.g., "hash_file", "upload_to_dropbox"
status = Column(
String, nullable=False
) # "pending", "in_progress", "success", "failure", "skipped"
step_name = Column(String, nullable=False, index=True) # e.g., "hash_file", "upload_to_dropbox"
status = Column(String, nullable=False) # "pending", "in_progress", "success", "failure", "skipped"
started_at = Column(DateTime(timezone=True), nullable=True) # When step started
completed_at = Column(
DateTime(timezone=True), nullable=True
) # When step finished (success/failure)
completed_at = Column(DateTime(timezone=True), nullable=True) # When step finished (success/failure)
error_message = Column(Text, nullable=True) # Error message if status is "failure"
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
__table_args__ = (
UniqueConstraint("file_id", "step_name", name="unique_file_step"),
)
__table_args__ = (UniqueConstraint("file_id", "step_name", name="unique_file_step"),)
class ProcessingLog(Base):
__tablename__ = "processing_logs"
id = Column(Integer, primary_key=True, index=True)
file_id = Column(
Integer, ForeignKey(_FILES_ID_FK), nullable=True
) # Optional file association
file_id = Column(Integer, ForeignKey(_FILES_ID_FK), nullable=True) # Optional file association
task_id = Column(String, index=True) # Celery task ID
step_name = Column(String) # e.g., "OCR", "convert_to_pdf", "upload_s3"
status = Column(String) # "pending", "in_progress", "success", "failure"
@@ -111,16 +98,10 @@ class ApplicationSettings(Base):
__tablename__ = "application_settings"
id = Column(Integer, primary_key=True, index=True)
key = Column(
String, unique=True, index=True, nullable=False
) # Setting key (e.g., 'database_url')
value = Column(
String, nullable=True
) # Setting value (stored as string, converted as needed)
key = Column(String, unique=True, index=True, nullable=False) # Setting key (e.g., 'database_url')
value = Column(String, nullable=True) # Setting value (stored as string, converted as needed)
created_at = Column(DateTime(timezone=True), server_default=func.now())
updated_at = Column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
class SettingsAuditLog(Base):
@@ -132,8 +113,6 @@ class SettingsAuditLog(Base):
key = Column(String, nullable=False, index=True) # Setting key that was changed
old_value = Column(String, nullable=True) # Previous value (None if first-time set)
new_value = Column(String, nullable=True) # New value (None if deleted)
changed_by = Column(
String, nullable=False
) # Username of the admin who made the change
changed_by = Column(String, nullable=False) # Username of the admin who made the change
changed_at = Column(DateTime(timezone=True), server_default=func.now(), index=True)
action = Column(String, nullable=False) # "update" or "delete"
+19 -66
View File
@@ -871,9 +871,7 @@ def get_setting_from_db(db: Session, key: str) -> Optional[str]:
Setting value as string (decrypted if necessary), or None if not found
"""
try:
setting = (
db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
)
setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
if not setting:
return None
@@ -890,9 +888,7 @@ def get_setting_from_db(db: Session, key: str) -> Optional[str]:
return None
def save_setting_to_db(
db: Session, key: str, value: Optional[str], changed_by: str = "system"
) -> bool:
def save_setting_to_db(db: Session, key: str, value: Optional[str], changed_by: str = "system") -> bool:
"""
Save or update a setting in the database.
@@ -914,20 +910,15 @@ def save_setting_to_db(
storage_value = value
if metadata.get("sensitive", False) and value:
from app.utils.encryption import (encrypt_value,
is_encryption_available)
from app.utils.encryption import encrypt_value, is_encryption_available
if is_encryption_available():
storage_value = encrypt_value(value)
logger.debug(f"Encrypted sensitive setting: {key}")
else:
logger.warning(
f"Storing sensitive setting {key} in plaintext (encryption unavailable)"
)
logger.warning(f"Storing sensitive setting {key} in plaintext (encryption unavailable)")
setting = (
db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
)
setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
old_storage_value = setting.value if setting else None
if setting:
@@ -1015,9 +1006,7 @@ def delete_setting_from_db(db: Session, key: str, changed_by: str = "system") ->
True if successful, False otherwise
"""
try:
setting = (
db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
)
setting = db.query(ApplicationSettings).filter(ApplicationSettings.key == key).first()
if setting:
# Capture old value for audit log (decrypt if sensitive)
metadata = get_setting_metadata(key)
@@ -1045,9 +1034,7 @@ def delete_setting_from_db(db: Session, key: str, changed_by: str = "system") ->
db.add(audit_entry)
db.commit()
logger.info(
f"Deleted setting {key} from database (changed_by={changed_by})"
)
logger.info(f"Deleted setting {key} from database (changed_by={changed_by})")
return True
return False
except SQLAlchemyError as e:
@@ -1131,9 +1118,7 @@ def validate_setting_value(key: str, value: str) -> Tuple[bool, Optional[str]]:
return True, None
def get_audit_log(
db: Session, limit: int = 100, offset: int = 0
) -> List[Dict[str, Any]]:
def get_audit_log(db: Session, limit: int = 100, offset: int = 0) -> List[Dict[str, Any]]:
"""
Retrieve the settings audit log, most recent first.
@@ -1150,11 +1135,7 @@ def get_audit_log(
"""
try:
entries = (
db.query(SettingsAuditLog)
.order_by(SettingsAuditLog.changed_at.desc())
.limit(limit)
.offset(offset)
.all()
db.query(SettingsAuditLog).order_by(SettingsAuditLog.changed_at.desc()).limit(limit).offset(offset).all()
)
result = []
for entry in entries:
@@ -1164,20 +1145,10 @@ def get_audit_log(
{
"id": entry.id,
"key": entry.key,
"old_value": (
"[REDACTED]"
if is_sensitive and entry.old_value
else entry.old_value
),
"new_value": (
"[REDACTED]"
if is_sensitive and entry.new_value
else entry.new_value
),
"old_value": ("[REDACTED]" if is_sensitive and entry.old_value else entry.old_value),
"new_value": ("[REDACTED]" if is_sensitive and entry.new_value else entry.new_value),
"changed_by": entry.changed_by,
"changed_at": (
entry.changed_at.isoformat() if entry.changed_at else None
),
"changed_at": (entry.changed_at.isoformat() if entry.changed_at else None),
"action": entry.action,
}
)
@@ -1215,20 +1186,10 @@ def get_setting_history(db: Session, key: str) -> List[Dict[str, Any]]:
{
"id": entry.id,
"key": entry.key,
"old_value": (
"[REDACTED]"
if is_sensitive and entry.old_value
else entry.old_value
),
"new_value": (
"[REDACTED]"
if is_sensitive and entry.new_value
else entry.new_value
),
"old_value": ("[REDACTED]" if is_sensitive and entry.old_value else entry.old_value),
"new_value": ("[REDACTED]" if is_sensitive and entry.new_value else entry.new_value),
"changed_by": entry.changed_by,
"changed_at": (
entry.changed_at.isoformat() if entry.changed_at else None
),
"changed_at": (entry.changed_at.isoformat() if entry.changed_at else None),
"action": entry.action,
}
)
@@ -1238,9 +1199,7 @@ def get_setting_history(db: Session, key: str) -> List[Dict[str, Any]]:
return []
def rollback_setting(
db: Session, key: str, history_id: int, changed_by: str = "system"
) -> bool:
def rollback_setting(db: Session, key: str, history_id: int, changed_by: str = "system") -> bool:
"""
Revert a setting to the value recorded in a specific audit log entry.
@@ -1264,14 +1223,10 @@ def rollback_setting(
"""
try:
history_entry = (
db.query(SettingsAuditLog)
.filter(SettingsAuditLog.id == history_id, SettingsAuditLog.key == key)
.first()
db.query(SettingsAuditLog).filter(SettingsAuditLog.id == history_id, SettingsAuditLog.key == key).first()
)
if not history_entry:
logger.warning(
f"Rollback failed: audit log entry {history_id} not found for key '{key}'"
)
logger.warning(f"Rollback failed: audit log entry {history_id} not found for key '{key}'")
return False
target_value = history_entry.new_value
@@ -1282,9 +1237,7 @@ def rollback_setting(
else:
return save_setting_to_db(db, key, target_value, changed_by=changed_by)
except SQLAlchemyError as e:
logger.error(
f"Error rolling back setting {key} to history entry {history_id}: {e}"
)
logger.error(f"Error rolling back setting {key} to history entry {history_id}: {e}")
db.rollback()
return False
+9 -12
View File
@@ -12,12 +12,13 @@ from fastapi.responses import RedirectResponse
from sqlalchemy.orm import Session
from app.utils.config_validator.masking import mask_sensitive_value
from app.utils.settings_service import (SETTING_METADATA,
get_all_settings_from_db,
get_setting_metadata,
get_settings_by_category)
from app.views.base import (APIRouter, get_db, require_login, settings,
templates)
from app.utils.settings_service import (
SETTING_METADATA,
get_all_settings_from_db,
get_setting_metadata,
get_settings_by_category,
)
from app.views.base import APIRouter, get_db, require_login, settings, templates
logger = logging.getLogger(__name__)
router = APIRouter()
@@ -102,9 +103,7 @@ async def settings_page(request: Request, db: Session = Depends(get_db)):
settings_data[category].append(
{
"key": key,
"display_value": (
display_value if display_value is not None else ""
),
"display_value": (display_value if display_value is not None else ""),
"metadata": metadata,
"source": source,
"source_label": source_label,
@@ -175,9 +174,7 @@ async def credentials_page(request: Request, db: Session = Depends(get_db)):
)
total = sum(len(v) for v in categories.values())
configured_count = sum(
1 for creds in categories.values() for c in creds if c["configured"]
)
configured_count = sum(1 for creds in categories.values() for c in creds if c["configured"])
return templates.TemplateResponse(
"credentials.html",
+5 -17
View File
@@ -40,11 +40,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge
current_settings = wizard_steps.get(step, [])
# Get step category (all settings in a step should have same category)
step_category = (
current_settings[0].get("wizard_category", "Configuration")
if current_settings
else "Configuration"
)
step_category = current_settings[0].get("wizard_category", "Configuration") if current_settings else "Configuration"
# Enrich settings with current live values
from app.config import settings as app_settings
@@ -68,9 +64,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge
else:
current_value = ""
value_source = "none"
enriched_settings.append(
{**s, "current_value": current_value, "value_source": value_source}
)
enriched_settings.append({**s, "current_value": current_value, "value_source": value_source})
current_settings = enriched_settings
return templates.TemplateResponse(
@@ -88,9 +82,7 @@ async def setup_wizard(request: Request, step: int = 1, db: Session = Depends(ge
@router.post("/setup")
async def setup_wizard_save(
request: Request, step: int = Form(...), db: Session = Depends(get_db)
):
async def setup_wizard_save(request: Request, step: int = Form(...), db: Session = Depends(get_db)):
"""
Save settings from the current wizard step.
"""
@@ -138,9 +130,7 @@ async def setup_wizard_save(
except Exception as e:
logger.error(f"Error saving wizard settings: {e}")
return RedirectResponse(
url=f"/setup?step={step}&error=save_failed", status_code=303
)
return RedirectResponse(url=f"/setup?step={step}&error=save_failed", status_code=303)
@router.get("/setup/skip")
@@ -176,9 +166,7 @@ async def setup_wizard_undo_skip(request: Request, db: Session = Depends(get_db)
try:
from app.utils.settings_service import delete_setting_from_db
delete_setting_from_db(
db, "_setup_wizard_skipped", changed_by="wizard_undo_skip"
)
delete_setting_from_db(db, "_setup_wizard_skipped", changed_by="wizard_undo_skip")
logger.info("Setup wizard skip marker removed; redirecting to wizard")
return RedirectResponse(url="/setup?step=1", status_code=303)
except Exception as e: