feat(settings): persist storage provider settings to DB, add export endpoint, enrich setup wizard

- dropbox/google_drive/onedrive save-settings: switch to DB as primary,
  .env write as best-effort (no longer fails when .env is absent)
- onedrive/google_drive update-settings: persist changes to DB alongside
  in-memory update; call notify_settings_updated()
- onedrive test_onedrive_token: persist rotated refresh token to DB
- settings_service: add get_settings_for_export() (db / effective modes)
- settings API: add GET /api/settings/export-env (admin-only, downloads .env)
- wizard: enrich settings with current values (DB > ENV > default) and
  value_source badges; pass setup_skipped to template; call
  notify_settings_updated() on save; add /setup/undo-skip route
- setup_wizard.html: pre-populate inputs with current_value; show
  DB/ENV/DEFAULT source badges; skip/undo-skip messaging
- settings.html: replace single Audit Log button with Setup Wizard link,
  Export .env dropdown, and Audit Log button group

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-23 02:41:04 +00:00
parent 0d5c20f685
commit ecc8100e24
8 changed files with 651 additions and 180 deletions
+125 -24
View File
@@ -7,11 +7,15 @@ import os
from datetime import datetime
from typing import Annotated, Optional
from fastapi import APIRouter, Form, HTTPException, Request, status
from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
from sqlalchemy.orm import Session
from app.auth import require_login
from app.config import settings
from app.database import get_db
from app.utils.oauth_helper import exchange_oauth_token
from app.utils.settings_service import save_setting_to_db
from app.utils.settings_sync import notify_settings_updated
# Set up logging
logger = logging.getLogger(__name__)
@@ -45,7 +49,9 @@ 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 {
@@ -64,38 +70,73 @@ async def update_google_drive_settings(
client_secret: Annotated[Optional[str], Form()] = None,
folder_id: Annotated[Optional[str], Form()] = None,
use_oauth: Annotated[str, Form()] = "true",
db: Session = Depends(get_db),
):
"""
Update Google Drive settings in memory
Update Google Drive settings in memory and persist to database
"""
try:
logger.info("Updating Google Drive settings in memory")
logger.info("Updating Google Drive settings in memory and database")
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"
)
# Convert use_oauth string to boolean
use_oauth_bool = use_oauth.lower() in ("true", "1", "yes", "y", "t")
# Update settings in memory
# Update settings in memory and persist to database
if refresh_token:
settings.google_drive_refresh_token = refresh_token
logger.info("Updated GOOGLE_DRIVE_REFRESH_TOKEN in memory")
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
logger.info("Updated GOOGLE_DRIVE_CLIENT_ID in memory")
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
logger.info("Updated GOOGLE_DRIVE_CLIENT_SECRET in memory")
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
logger.info("Updated GOOGLE_DRIVE_FOLDER_ID in memory")
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
settings.google_drive_use_oauth = use_oauth_bool
logger.info(f"Updated GOOGLE_DRIVE_USE_OAUTH in memory to {use_oauth_bool}")
save_setting_to_db(
db,
"google_drive_use_oauth",
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}"
)
return {"status": "success", "message": "Google Drive settings have been updated in memory"}
notify_settings_updated()
return {
"status": "success",
"message": "Google Drive settings have been updated in memory and database",
}
except Exception as e:
logger.exception(f"Unexpected error updating Google Drive settings: {str(e)}")
@@ -113,7 +154,8 @@ 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")
@@ -125,7 +167,10 @@ async def test_google_drive_token(request: Request):
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured")
return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
return {
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured",
}
try:
# Test OAuth connection
@@ -187,8 +232,13 @@ 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")
return {"status": "error", "message": "Google Drive service account credentials are not configured"}
logger.warning(
"Google Drive service account credentials not configured"
)
return {
"status": "error",
"message": "Google Drive service account credentials are not configured",
}
try:
service = get_google_drive_service()
@@ -203,7 +253,9 @@ 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",
@@ -214,7 +266,10 @@ async def test_google_drive_token(request: Request):
except Exception as e:
error_msg = str(e)
logger.error(f"Google Drive service account test failed: {error_msg}")
return {"status": "error", "message": f"Service account validation failed: {error_msg}"}
return {
"status": "error",
"message": f"Service account validation failed: {error_msg}",
}
except Exception as e:
logger.exception("Unexpected error testing Google Drive token")
@@ -246,7 +301,10 @@ async def get_google_drive_token_info(request: Request):
and settings.google_drive_refresh_token
):
logger.warning("Google Drive OAuth credentials not fully configured")
return {"status": "error", "message": "Google Drive OAuth credentials are not fully configured"}
return {
"status": "error",
"message": "Google Drive OAuth credentials are not fully configured",
}
try:
# Get credentials and access token
@@ -333,17 +391,29 @@ async def save_dropbox_settings(
client_secret: Annotated[Optional[str], Form()] = None,
folder_id: Annotated[Optional[str], Form()] = None,
use_oauth: Annotated[str, Form()] = "true",
db: Session = Depends(get_db),
):
"""
Save Google Drive settings to the .env file
Save Google Drive settings to the .env file (best-effort) and persist to database.
"""
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"
)
# Define settings to update
drive_settings = {"GOOGLE_DRIVE_USE_OAUTH": str(use_oauth_bool).lower()}
@@ -376,7 +446,9 @@ 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)
@@ -396,7 +468,9 @@ 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"
@@ -415,7 +489,33 @@ async def save_dropbox_settings(
# Set OAuth flag
settings.google_drive_use_oauth = use_oauth_bool
logger.info("Successfully updated Google Drive settings in memory")
# Persist to database
save_setting_to_db(
db,
"google_drive_use_oauth",
str(use_oauth_bool).lower(),
changed_by=changed_by,
)
if refresh_token:
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
)
if client_secret:
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
)
notify_settings_updated()
logger.info("Successfully updated Google Drive settings in memory and database")
return {
"status": "success",
@@ -426,5 +526,6 @@ async def save_dropbox_settings(
except Exception as e:
logger.exception(f"Unexpected error saving Google Drive settings: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to save Google Drive settings: {str(e)}"
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to save Google Drive settings: {str(e)}",
)