diff --git a/backend/api/config.py b/backend/api/config.py index e85354f..ed836fc 100644 --- a/backend/api/config.py +++ b/backend/api/config.py @@ -137,81 +137,6 @@ async def test_newbook_settings( return await _test_newbook(db) -# ============================================ -# RESOS SETTINGS ENDPOINTS -# ============================================ - -class ResosSettingsResponse(BaseModel): - resos_api_key_set: bool = False - - -class ResosSettingsUpdate(BaseModel): - resos_api_key: Optional[str] = None - - -@router.get("/settings/resos", response_model=ResosSettingsResponse) -async def get_resos_settings( - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user) -): - """Get Resos settings""" - result = await db.execute( - text("SELECT config_value FROM system_config WHERE config_key = 'resos_api_key'") - ) - row = result.fetchone() - - resos_api_key_set = bool(row and row.config_value) - - return ResosSettingsResponse(resos_api_key_set=resos_api_key_set) - - -@router.post("/settings/resos") -async def update_resos_settings( - settings: ResosSettingsUpdate, - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user) -): - """Update Resos settings""" - # Update Resos API key (encrypted) - if settings.resos_api_key: - encrypted_key = base64.b64encode(settings.resos_api_key.encode()).decode() - await db.execute( - text(""" - INSERT INTO system_config (config_key, config_value, is_encrypted, updated_at, updated_by) - VALUES ('resos_api_key', :value, true, NOW(), :user) - ON CONFLICT (config_key) DO UPDATE SET - config_value = :value, - is_encrypted = true, - updated_at = NOW(), - updated_by = :user - """), - {"value": encrypted_key, "user": current_user['username']} - ) - - await db.commit() - return {"status": "saved", "message": "Resos settings updated"} - - -@router.post("/settings/resos/test") -async def test_resos_settings( - db: AsyncSession = Depends(get_db), - current_user: dict = Depends(get_current_user) -): - """Test Resos connection with current settings""" - try: - from services.resos_client import ResosClient - async with await ResosClient.from_db(db) as client: - if not client.api_key: - raise HTTPException(status_code=400, detail="Resos API key not configured") - success = await client.test_connection() - if success: - return {"status": "success", "message": "Connected to Resos API successfully"} - else: - raise HTTPException(status_code=400, detail="Connection failed - check API key") - except HTTPException: - raise - except Exception as e: - raise HTTPException(status_code=500, detail=f"Connection test failed: {str(e)}") # ============================================ @@ -840,8 +765,10 @@ async def _test_newbook(db: AsyncSession): async def _test_resos(db: AsyncSession): """Test Resos API connection""" import httpx + from services.central_settings import get_resos_credentials - api_key = await _get_config_value(db, "resos_api_key") + creds = await get_resos_credentials() + api_key = creds["api_key"] if creds else None if not api_key: raise HTTPException(status_code=400, detail="Resos API key not configured") diff --git a/backend/jobs/data_sync.py b/backend/jobs/data_sync.py index 4697a94..8c20f3d 100644 --- a/backend/jobs/data_sync.py +++ b/backend/jobs/data_sync.py @@ -76,29 +76,10 @@ def load_newbook_credentials(db) -> dict: def load_resos_credentials(db) -> dict: - """Load Resos API credentials from database config.""" - import base64 - - def decrypt(value: str) -> str: - """Decrypt base64 encoded value""" - if not value: - return None - try: - return base64.b64decode(value.encode()).decode() - except: - return value - - # Get API key (may be encrypted) - api_key_result = db.execute( - text("SELECT config_value, is_encrypted FROM system_config WHERE config_key = 'resos_api_key'") - ) - api_key_row = api_key_result.fetchone() - - api_key = None - if api_key_row and api_key_row.config_value: - api_key = decrypt(api_key_row.config_value) if api_key_row.is_encrypted else api_key_row.config_value - - return {'api_key': api_key} + """Load Resos API credentials from central Settings service.""" + from services.central_settings import get_resos_credentials_sync + creds = get_resos_credentials_sync() + return creds or {'api_key': None} def load_gl_config(db) -> tuple: diff --git a/backend/jobs/resos_bookings_sync.py b/backend/jobs/resos_bookings_sync.py index f90af66..f85e4df 100644 --- a/backend/jobs/resos_bookings_sync.py +++ b/backend/jobs/resos_bookings_sync.py @@ -5,7 +5,6 @@ Pattern: Replicates newbook bookings sync but adapted for Resos covers/stats """ import json import logging -import base64 from datetime import date, datetime, timedelta from typing import Optional, Tuple, List, Dict, Any @@ -20,27 +19,6 @@ logger = logging.getLogger(__name__) VALID_STATUSES = ('approved', 'arrived', 'seated', 'left') -def get_config_value(db, key: str) -> Optional[str]: - """Get a configuration value from system_config table.""" - result = db.execute( - text("SELECT config_value, is_encrypted FROM system_config WHERE config_key = :key"), - {"key": key} - ) - row = result.fetchone() - if not row or not row.config_value: - return None - - # Decrypt if encrypted - if row.is_encrypted: - try: - return base64.b64decode(row.config_value.encode()).decode() - except Exception as e: - logger.warning(f"Failed to decrypt {key}: {e}") - return row.config_value - - return row.config_value - - def load_resos_custom_field_mappings(db) -> Dict[str, Dict[str, Any]]: """ Load custom field mappings from resos_custom_field_mapping table. @@ -194,11 +172,13 @@ async def sync_resos_bookings_data( ) db.commit() - # Load Resos API key - api_key = get_config_value(db, 'resos_api_key') + # Load Resos API key from central Settings service + from services.central_settings import get_resos_credentials_sync + _resos_creds = get_resos_credentials_sync() + api_key = _resos_creds["api_key"] if _resos_creds else None if not api_key: - raise Exception("Resos API key not configured") + raise Exception("Resos API key not configured in central Settings") # Load mappings cf_mappings = load_resos_custom_field_mappings(db) diff --git a/backend/services/central_settings.py b/backend/services/central_settings.py index 6814b09..e7326ff 100644 --- a/backend/services/central_settings.py +++ b/backend/services/central_settings.py @@ -98,3 +98,22 @@ def get_integration_sync(name: str) -> Optional[dict]: def get_newbook_credentials_sync() -> Optional[dict]: """Blocking variant of get_newbook_credentials for sync job contexts.""" return _extract_newbook(get_integration_sync("newbook")) + + +def _extract_resos(s: Optional[dict]) -> Optional[dict]: + if not s: + return None + key = s.get("api_key") or "" + if not key: + return None + return {"api_key": key} + + +async def get_resos_credentials() -> Optional[dict]: + """Returns {'api_key'} from central settings, or None if not configured.""" + return _extract_resos(await get_integration("resos")) + + +def get_resos_credentials_sync() -> Optional[dict]: + """Blocking variant of get_resos_credentials for sync job contexts.""" + return _extract_resos(get_integration_sync("resos")) diff --git a/backend/services/resos_client.py b/backend/services/resos_client.py index 1c09d13..e0e832a 100644 --- a/backend/services/resos_client.py +++ b/backend/services/resos_client.py @@ -44,11 +44,10 @@ class ResosClient: @classmethod async def from_db(cls, db): - """Create client with credentials from database""" - from api.config import _get_config_value - - api_key = await _get_config_value(db, "resos_api_key") - return cls(api_key=api_key) + """Create client with credentials from central Settings service""" + from services.central_settings import get_resos_credentials + creds = await get_resos_credentials() + return cls(api_key=creds["api_key"] if creds else None) async def __aenter__(self): self.client = httpx.AsyncClient(timeout=30.0) diff --git a/frontend/src/index.css b/frontend/src/index.css index 1c0146a..8f3b4af 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -361,3 +361,18 @@ tr:hover td { background: #f8fafc; } padding: 16px; } } + +/* Sidebar scrollbar */ +.nav-scroll::-webkit-scrollbar, +.sidebar::-webkit-scrollbar, +.sidebar-nav::-webkit-scrollbar { width: 4px; } +.nav-scroll::-webkit-scrollbar-track, +.sidebar::-webkit-scrollbar-track, +.sidebar-nav::-webkit-scrollbar-track { background: transparent; } +.nav-scroll::-webkit-scrollbar-thumb, +.sidebar::-webkit-scrollbar-thumb, +.sidebar-nav::-webkit-scrollbar-thumb { background: rgba(201,168,76,0.35); border-radius: 2px; } +.nav-scroll::-webkit-scrollbar-thumb:hover, +.sidebar::-webkit-scrollbar-thumb:hover, +.sidebar-nav::-webkit-scrollbar-thumb:hover { background: rgba(201,168,76,0.65); } +.nav-scroll, .sidebar, .sidebar-nav { scrollbar-width: thin; scrollbar-color: rgba(201,168,76,0.35) transparent; } diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index ee1016a..60cc159 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -2115,11 +2115,6 @@ const NewbookPage: React.FC = () => { // RESOS SETTINGS PAGE // ============================================ -interface ResosSettings { - resos_api_key: string | null - resos_api_key_set: boolean -} - interface ResosCustomField { id: string name: string @@ -2156,11 +2151,7 @@ const ResosPage: React.FC = () => { return (
Configure your Resos API connection and sync settings for restaurant reservation management.
- -Configure Resos sync settings for restaurant reservation management. The Resos API key is managed centrally in the Settings app.