From 1564a66283855cba51812472f29ead47b73468c7 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Sun, 12 Jul 2026 22:58:32 +0000 Subject: [PATCH] Wire Azure OCR to central settings toggle; remove redundant Users section - settings/src/integrations/schema.js: rename azure from 'Azure AD' to 'Azure Document Intelligence', swap fields to endpoint + api_key - Add use_global_azure column (migration + model) - global_settings_service: add azure to check_global_status and apply_global_overrides - api/settings.py: expose use_global_azure in response/update; apply overrides in test_azure_connection before credential check - Settings.tsx: add 'Use credentials from main stack settings' toggle for Azure OCR section (endpoint/key disabled when on, test button enabled when global is configured); remove Users section (managed centrally via auth service), clean up UserData interface, users query and mutations Co-Authored-By: Claude Sonnet 4.6 --- backend/api/settings.py | 11 +- .../migrations/add_global_settings_flags.py | 1 + backend/models/settings.py | 1 + backend/services/global_settings_service.py | 7 + frontend/src/pages/Settings.tsx | 185 +++--------------- 5 files changed, 44 insertions(+), 161 deletions(-) diff --git a/backend/api/settings.py b/backend/api/settings.py index a22fb93..d3151ef 100644 --- a/backend/api/settings.py +++ b/backend/api/settings.py @@ -51,6 +51,7 @@ class SettingsResponse(BaseModel): llm_features_enabled: dict | None = None # Global stack settings delegation use_global_smtp: bool = False + use_global_azure: bool = False class Config: from_attributes = True @@ -96,6 +97,7 @@ class SettingsUpdate(BaseModel): llm_features_enabled: dict | None = None # Global stack settings delegation use_global_smtp: bool | None = None + use_global_azure: bool | None = None @router.get("/", response_model=SettingsResponse) @@ -163,6 +165,7 @@ def _build_settings_response(settings: KitchenSettings) -> SettingsResponse: llm_monthly_token_limit=settings.llm_monthly_token_limit, llm_features_enabled=settings.llm_features_enabled, use_global_smtp=settings.use_global_smtp, + use_global_azure=settings.use_global_azure, ) @@ -205,7 +208,13 @@ async def test_azure_connection( ) settings = result.scalar_one_or_none() - if not settings or not settings.azure_endpoint or not settings.azure_key: + if not settings: + raise HTTPException(status_code=400, detail="Azure credentials not configured") + + from services.global_settings_service import apply_global_overrides + await apply_global_overrides(settings) + + if not settings.azure_endpoint or not settings.azure_key: raise HTTPException( status_code=400, detail="Azure credentials not configured" diff --git a/backend/migrations/add_global_settings_flags.py b/backend/migrations/add_global_settings_flags.py index f888b15..e1150b1 100644 --- a/backend/migrations/add_global_settings_flags.py +++ b/backend/migrations/add_global_settings_flags.py @@ -12,6 +12,7 @@ async def migrate(): "ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_newbook BOOLEAN DEFAULT FALSE", "ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_resos BOOLEAN DEFAULT FALSE", "ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_sambapos BOOLEAN DEFAULT FALSE", + "ALTER TABLE kitchen_settings ADD COLUMN IF NOT EXISTS use_global_azure BOOLEAN DEFAULT FALSE", ]: try: await conn.execute(text(col)) diff --git a/backend/models/settings.py b/backend/models/settings.py index 2f31cec..6584184 100644 --- a/backend/models/settings.py +++ b/backend/models/settings.py @@ -220,6 +220,7 @@ class KitchenSettings(Base): use_global_newbook: Mapped[bool] = mapped_column(Boolean, default=False) use_global_resos: Mapped[bool] = mapped_column(Boolean, default=False) use_global_sambapos: Mapped[bool] = mapped_column(Boolean, default=False) + use_global_azure: Mapped[bool] = mapped_column(Boolean, default=False) # Internal API key (for in-house apps like menu display plugin) api_key: Mapped[str | None] = mapped_column(String(100), nullable=True) diff --git a/backend/services/global_settings_service.py b/backend/services/global_settings_service.py index a87aa58..0d4420b 100644 --- a/backend/services/global_settings_service.py +++ b/backend/services/global_settings_service.py @@ -43,6 +43,7 @@ async def check_global_status() -> dict[str, bool]: "newbook": ["api_key"], "resos": ["api_key"], "sambapos": ["password"], + "azure": ["api_key"], } results: dict[str, bool] = {} for slug, fields in required.items(): @@ -95,3 +96,9 @@ async def apply_global_overrides(settings) -> None: settings.sambapos_db_name = creds.get("database") or settings.sambapos_db_name settings.sambapos_db_username = creds.get("username") or settings.sambapos_db_username settings.sambapos_db_password = creds.get("password") or settings.sambapos_db_password + + if getattr(settings, "use_global_azure", False): + creds = await get_global_integration("azure") + if creds: + settings.azure_endpoint = creds.get("endpoint") or settings.azure_endpoint + settings.azure_key = creds.get("api_key") or settings.azure_key diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index e7c35bc..2043203 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -42,6 +42,7 @@ interface SettingsData { llm_monthly_token_limit: number llm_features_enabled: Record | null use_global_smtp: boolean + use_global_azure: boolean } interface NewbookSettingsData { @@ -105,16 +106,7 @@ interface RoomCategory { display_order: number } -interface UserData { - id: number - email: string - name: string | null - is_active: boolean - is_admin: boolean - created_at: string -} - -type SettingsSection = 'account' | 'users' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data' +type SettingsSection = 'account' | 'access' | 'display' | 'azure' | 'email' | 'inbox' | 'dext' | 'newbook' | 'resos' | 'sambapos' | 'kds' | 'budget' | 'kitchen' | 'suppliers' | 'search' | 'nextcloud' | 'backup' | 'food_flags' | 'allergen_keywords' | 'ingredient_categories' | 'recipe_sections' | 'dish_courses' | 'api_access' | 'llm' | 'data' interface SambaPOSSettingsData { sambapos_db_host: string | null @@ -290,6 +282,7 @@ export default function Settings() { const [azureEndpoint, setAzureEndpoint] = useState('') const [azureKey, setAzureKey] = useState('') const [azureTestStatus, setAzureTestStatus] = useState(null) + const [useGlobalAzure, setUseGlobalAzure] = useState(false) // OCR post-processing options const [ocrCleanProductCodes, setOcrCleanProductCodes] = useState(false) const [ocrFilterSubtotalRows, setOcrFilterSubtotalRows] = useState(false) @@ -611,21 +604,6 @@ export default function Settings() { }, }) - // Fetch users (admin only) - const { data: users } = useQuery({ - queryKey: ['users'], - queryFn: async () => { - const res = await fetch('/auth/users', { - headers: { Authorization: `Bearer ${token}` }, - }) - if (!res.ok) { - if (res.status === 403) return [] - throw new Error('Failed to fetch users') - } - return res.json() - }, - enabled: !!user?.is_admin, - }) // Fetch SambaPOS settings const { data: sambaSettings } = useQuery({ @@ -905,6 +883,7 @@ export default function Settings() { useEffect(() => { if (settings) { setAzureEndpoint(settings.azure_endpoint || '') + setUseGlobalAzure(settings.use_global_azure) setCurrencySymbol(settings.currency_symbol) setDateFormat(settings.date_format) setHighQuantityThreshold(settings.high_quantity_threshold) @@ -1662,57 +1641,6 @@ export default function Settings() { }, }) - const toggleUserMutation = useMutation({ - mutationFn: async (userId: number) => { - const res = await fetch(`/auth/users/${userId}/toggle-active`, { - method: 'PATCH', - headers: { Authorization: `Bearer ${token}` }, - }) - if (!res.ok) { - const data = await res.json() - throw new Error(data.detail || 'Failed to toggle user') - } - return res.json() - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['users'] }) - }, - }) - - const deleteUserMutation = useMutation({ - mutationFn: async (userId: number) => { - const res = await fetch(`/auth/users/${userId}`, { - method: 'DELETE', - headers: { Authorization: `Bearer ${token}` }, - }) - if (!res.ok) { - const data = await res.json() - throw new Error(data.detail || 'Failed to delete user') - } - return res.json() - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['users'] }) - }, - }) - - const toggleAdminMutation = useMutation({ - mutationFn: async (userId: number) => { - const res = await fetch(`/auth/users/${userId}/toggle-admin`, { - method: 'PATCH', - headers: { Authorization: `Bearer ${token}` }, - }) - if (!res.ok) { - const data = await res.json() - throw new Error(data.detail || 'Failed to toggle admin status') - } - return res.json() - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['users'] }) - }, - }) - const savePageRestrictionsMutation = useMutation({ mutationFn: async (pages: string[]) => { const res = await fetch('/kitchen/api/settings/page-restrictions', { @@ -2543,6 +2471,7 @@ export default function Settings() { ocr_use_weight_as_quantity: ocrUseWeightAsQuantity, cost_distribution_max_days: costDistMaxDays, use_global_smtp: useGlobalSmtp, + use_global_azure: useGlobalAzure, } if (azureKey) { data.azure_key = azureKey @@ -2758,7 +2687,6 @@ export default function Settings() { const sidebarItems: { id: SettingsSection; label: string; adminOnly?: boolean; href?: string; restrictPath?: string }[] = [ { id: 'account', label: 'Account' }, - { id: 'users', label: 'Users', adminOnly: true, restrictPath: '/settings-users' }, { id: 'access', label: 'Access Control', adminOnly: true, restrictPath: '/settings-access' }, { id: 'display', label: 'Display', restrictPath: '/settings-display' }, { id: 'azure', label: 'Azure OCR', restrictPath: '/settings-azure' }, @@ -2933,82 +2861,6 @@ export default function Settings() { )} - {/* Users Section (Admin Only) */} - {activeSection === 'users' && user?.is_admin && ( -
-

User Management

-

Manage users who have access to this kitchen.

- - {/* Users List Block */} -
-

Users

- {users && users.length > 0 && ( - - - - - - - - - - - - {users.map((u) => ( - - - - - - - - ))} - -
EmailNameStatusRoleActions
{u.email}{u.name || '-'} - - {u.is_active ? 'Active' : 'Disabled'} - - {u.is_admin ? 'Admin' : 'User'} - {u.id !== user.id ? ( -
- - - {!u.is_admin && ( - - )} -
- ) : ( - (You) - )} -
- )} -
-
- )} - {/* Access Control Section (Admin Only) */} {activeSection === 'access' && user?.is_admin && (
@@ -3110,7 +2962,6 @@ export default function Settings() {
{[ { path: '/settings', label: 'Settings Page (entire page)' }, - { path: '/settings-users', label: 'Users Management' }, { path: '/settings-access', label: 'Access Control' }, { path: '/settings-display', label: 'Display Settings' }, { path: '/settings-azure', label: 'Azure OCR' }, @@ -3262,6 +3113,21 @@ export default function Settings() { {/* API Configuration Block */}

API Configuration

+
@@ -3279,15 +3146,13 @@ export default function Settings() { type="password" value={azureKey} onChange={(e) => setAzureKey(e.target.value)} - style={styles.input} + disabled={useGlobalAzure} + style={{ ...styles.input, ...(useGlobalAzure ? { opacity: 0.4, pointerEvents: 'none' as const } : {}) }} placeholder={settings?.azure_key_set ? '••••••••••••••••' : 'Enter your API key'} /> {settings?.azure_key_set && !azureKey && Key is configured}
-

- Azure Document Intelligence uses app-specific credentials (not shared with the main stack). -

{/* OCR Post-Processing Block */} @@ -3328,7 +3193,7 @@ export default function Settings() { {/* Actions - outside blocks */}
-