machinaos 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.template +71 -0
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/bin/cli.js +159 -0
- package/client/.dockerignore +45 -0
- package/client/Dockerfile +68 -0
- package/client/eslint.config.js +29 -0
- package/client/index.html +13 -0
- package/client/nginx.conf +66 -0
- package/client/package.json +48 -0
- package/client/src/App.tsx +27 -0
- package/client/src/Dashboard.tsx +1173 -0
- package/client/src/ParameterPanel.tsx +301 -0
- package/client/src/components/AIAgentNode.tsx +321 -0
- package/client/src/components/APIKeyValidator.tsx +118 -0
- package/client/src/components/ClaudeChatModelNode.tsx +18 -0
- package/client/src/components/ConditionalEdge.tsx +189 -0
- package/client/src/components/CredentialsModal.tsx +306 -0
- package/client/src/components/EdgeConditionEditor.tsx +443 -0
- package/client/src/components/GeminiChatModelNode.tsx +18 -0
- package/client/src/components/GenericNode.tsx +357 -0
- package/client/src/components/LocationParameterPanel.tsx +154 -0
- package/client/src/components/ModelNode.tsx +286 -0
- package/client/src/components/OpenAIChatModelNode.tsx +18 -0
- package/client/src/components/OutputPanel.tsx +471 -0
- package/client/src/components/ParameterRenderer.tsx +1874 -0
- package/client/src/components/SkillEditorModal.tsx +417 -0
- package/client/src/components/SquareNode.tsx +797 -0
- package/client/src/components/StartNode.tsx +250 -0
- package/client/src/components/ToolkitNode.tsx +365 -0
- package/client/src/components/TriggerNode.tsx +463 -0
- package/client/src/components/auth/LoginPage.tsx +247 -0
- package/client/src/components/auth/ProtectedRoute.tsx +59 -0
- package/client/src/components/base/BaseChatModelNode.tsx +271 -0
- package/client/src/components/icons/AIProviderIcons.tsx +50 -0
- package/client/src/components/maps/GoogleMapsPicker.tsx +137 -0
- package/client/src/components/maps/MapsPreviewPanel.tsx +110 -0
- package/client/src/components/maps/index.ts +26 -0
- package/client/src/components/parameterPanel/InputSection.tsx +1094 -0
- package/client/src/components/parameterPanel/LocationPanelLayout.tsx +65 -0
- package/client/src/components/parameterPanel/MapsSection.tsx +92 -0
- package/client/src/components/parameterPanel/MiddleSection.tsx +571 -0
- package/client/src/components/parameterPanel/OutputSection.tsx +81 -0
- package/client/src/components/parameterPanel/ParameterPanelLayout.tsx +82 -0
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +436 -0
- package/client/src/components/parameterPanel/index.ts +42 -0
- package/client/src/components/shared/DataPanel.tsx +142 -0
- package/client/src/components/shared/JSONTreeRenderer.tsx +106 -0
- package/client/src/components/ui/AIResultModal.tsx +204 -0
- package/client/src/components/ui/AndroidSettingsPanel.tsx +401 -0
- package/client/src/components/ui/CodeEditor.tsx +81 -0
- package/client/src/components/ui/CollapsibleSection.tsx +88 -0
- package/client/src/components/ui/ComponentItem.tsx +154 -0
- package/client/src/components/ui/ComponentPalette.tsx +321 -0
- package/client/src/components/ui/ConsolePanel.tsx +1074 -0
- package/client/src/components/ui/ErrorBoundary.tsx +196 -0
- package/client/src/components/ui/InputNodesPanel.tsx +204 -0
- package/client/src/components/ui/MapSelector.tsx +314 -0
- package/client/src/components/ui/Modal.tsx +149 -0
- package/client/src/components/ui/NodeContextMenu.tsx +192 -0
- package/client/src/components/ui/NodeOutputPanel.tsx +1150 -0
- package/client/src/components/ui/OutputDisplayPanel.tsx +381 -0
- package/client/src/components/ui/SettingsPanel.tsx +243 -0
- package/client/src/components/ui/TopToolbar.tsx +736 -0
- package/client/src/components/ui/WhatsAppSettingsPanel.tsx +345 -0
- package/client/src/components/ui/WorkflowSidebar.tsx +294 -0
- package/client/src/config/antdTheme.ts +186 -0
- package/client/src/config/api.ts +54 -0
- package/client/src/contexts/AuthContext.tsx +221 -0
- package/client/src/contexts/ThemeContext.tsx +42 -0
- package/client/src/contexts/WebSocketContext.tsx +1971 -0
- package/client/src/factories/baseChatModelFactory.ts +256 -0
- package/client/src/hooks/useAndroidOperations.ts +164 -0
- package/client/src/hooks/useApiKeyValidation.ts +107 -0
- package/client/src/hooks/useApiKeys.ts +238 -0
- package/client/src/hooks/useAppTheme.ts +17 -0
- package/client/src/hooks/useComponentPalette.ts +51 -0
- package/client/src/hooks/useCopyPaste.ts +155 -0
- package/client/src/hooks/useDragAndDrop.ts +124 -0
- package/client/src/hooks/useDragVariable.ts +88 -0
- package/client/src/hooks/useExecution.ts +313 -0
- package/client/src/hooks/useParameterPanel.ts +176 -0
- package/client/src/hooks/useReactFlowNodes.ts +189 -0
- package/client/src/hooks/useToolSchema.ts +209 -0
- package/client/src/hooks/useWhatsApp.ts +196 -0
- package/client/src/hooks/useWorkflowManagement.ts +46 -0
- package/client/src/index.css +315 -0
- package/client/src/main.tsx +19 -0
- package/client/src/nodeDefinitions/aiAgentNodes.ts +336 -0
- package/client/src/nodeDefinitions/aiModelNodes.ts +340 -0
- package/client/src/nodeDefinitions/androidDeviceNodes.ts +140 -0
- package/client/src/nodeDefinitions/androidServiceNodes.ts +383 -0
- package/client/src/nodeDefinitions/chatNodes.ts +135 -0
- package/client/src/nodeDefinitions/codeNodes.ts +54 -0
- package/client/src/nodeDefinitions/documentNodes.ts +379 -0
- package/client/src/nodeDefinitions/index.ts +15 -0
- package/client/src/nodeDefinitions/locationNodes.ts +463 -0
- package/client/src/nodeDefinitions/schedulerNodes.ts +220 -0
- package/client/src/nodeDefinitions/skillNodes.ts +211 -0
- package/client/src/nodeDefinitions/toolNodes.ts +198 -0
- package/client/src/nodeDefinitions/utilityNodes.ts +284 -0
- package/client/src/nodeDefinitions/whatsappNodes.ts +865 -0
- package/client/src/nodeDefinitions/workflowNodes.ts +41 -0
- package/client/src/nodeDefinitions.ts +104 -0
- package/client/src/schemas/workflowSchema.ts +264 -0
- package/client/src/services/dynamicParameterService.ts +96 -0
- package/client/src/services/execution/aiAgentExecutionService.ts +35 -0
- package/client/src/services/executionService.ts +232 -0
- package/client/src/services/workflowApi.ts +91 -0
- package/client/src/store/useAppStore.ts +582 -0
- package/client/src/styles/theme.ts +508 -0
- package/client/src/styles/zIndex.ts +17 -0
- package/client/src/types/ComponentTypes.ts +39 -0
- package/client/src/types/EdgeCondition.ts +231 -0
- package/client/src/types/INodeProperties.ts +288 -0
- package/client/src/types/NodeTypes.ts +28 -0
- package/client/src/utils/formatters.ts +33 -0
- package/client/src/utils/googleMapsLoader.ts +140 -0
- package/client/src/utils/locationUtils.ts +85 -0
- package/client/src/utils/nodeUtils.ts +31 -0
- package/client/src/utils/workflow.ts +30 -0
- package/client/src/utils/workflowExport.ts +120 -0
- package/client/src/vite-env.d.ts +12 -0
- package/client/tailwind.config.js +60 -0
- package/client/tsconfig.json +25 -0
- package/client/tsconfig.node.json +11 -0
- package/client/vite.config.js +35 -0
- package/docker-compose.prod.yml +107 -0
- package/docker-compose.yml +104 -0
- package/docs-MachinaOs/README.md +85 -0
- package/docs-MachinaOs/deployment/docker.mdx +228 -0
- package/docs-MachinaOs/deployment/production.mdx +345 -0
- package/docs-MachinaOs/docs.json +75 -0
- package/docs-MachinaOs/faq.mdx +309 -0
- package/docs-MachinaOs/favicon.svg +5 -0
- package/docs-MachinaOs/installation.mdx +160 -0
- package/docs-MachinaOs/introduction.mdx +114 -0
- package/docs-MachinaOs/logo/dark.svg +6 -0
- package/docs-MachinaOs/logo/light.svg +6 -0
- package/docs-MachinaOs/nodes/ai-agent.mdx +216 -0
- package/docs-MachinaOs/nodes/ai-models.mdx +240 -0
- package/docs-MachinaOs/nodes/android.mdx +411 -0
- package/docs-MachinaOs/nodes/overview.mdx +181 -0
- package/docs-MachinaOs/nodes/schedulers.mdx +316 -0
- package/docs-MachinaOs/nodes/webhooks.mdx +330 -0
- package/docs-MachinaOs/nodes/whatsapp.mdx +305 -0
- package/docs-MachinaOs/quickstart.mdx +119 -0
- package/docs-MachinaOs/tutorials/ai-agent-workflow.mdx +177 -0
- package/docs-MachinaOs/tutorials/android-automation.mdx +242 -0
- package/docs-MachinaOs/tutorials/first-workflow.mdx +134 -0
- package/docs-MachinaOs/tutorials/whatsapp-automation.mdx +185 -0
- package/nul +0 -0
- package/package.json +70 -0
- package/scripts/build.js +158 -0
- package/scripts/check-ports.ps1 +33 -0
- package/scripts/clean.js +40 -0
- package/scripts/docker.js +93 -0
- package/scripts/kill-port.ps1 +154 -0
- package/scripts/start.js +210 -0
- package/scripts/stop.js +325 -0
- package/server/.dockerignore +44 -0
- package/server/Dockerfile +45 -0
- package/server/constants.py +249 -0
- package/server/core/__init__.py +1 -0
- package/server/core/cache.py +461 -0
- package/server/core/config.py +128 -0
- package/server/core/container.py +99 -0
- package/server/core/database.py +1211 -0
- package/server/core/logging.py +314 -0
- package/server/main.py +289 -0
- package/server/middleware/__init__.py +5 -0
- package/server/middleware/auth.py +89 -0
- package/server/models/__init__.py +1 -0
- package/server/models/auth.py +52 -0
- package/server/models/cache.py +24 -0
- package/server/models/database.py +211 -0
- package/server/models/nodes.py +455 -0
- package/server/package.json +9 -0
- package/server/pyproject.toml +72 -0
- package/server/requirements.txt +83 -0
- package/server/routers/__init__.py +1 -0
- package/server/routers/android.py +294 -0
- package/server/routers/auth.py +203 -0
- package/server/routers/database.py +151 -0
- package/server/routers/maps.py +142 -0
- package/server/routers/nodejs_compat.py +289 -0
- package/server/routers/webhook.py +90 -0
- package/server/routers/websocket.py +2127 -0
- package/server/routers/whatsapp.py +761 -0
- package/server/routers/workflow.py +200 -0
- package/server/services/__init__.py +1 -0
- package/server/services/ai.py +2415 -0
- package/server/services/android/__init__.py +27 -0
- package/server/services/android/broadcaster.py +114 -0
- package/server/services/android/client.py +608 -0
- package/server/services/android/manager.py +78 -0
- package/server/services/android/protocol.py +165 -0
- package/server/services/android_service.py +588 -0
- package/server/services/auth.py +131 -0
- package/server/services/chat_client.py +160 -0
- package/server/services/deployment/__init__.py +12 -0
- package/server/services/deployment/manager.py +706 -0
- package/server/services/deployment/state.py +47 -0
- package/server/services/deployment/triggers.py +275 -0
- package/server/services/event_waiter.py +785 -0
- package/server/services/execution/__init__.py +77 -0
- package/server/services/execution/cache.py +769 -0
- package/server/services/execution/conditions.py +373 -0
- package/server/services/execution/dlq.py +132 -0
- package/server/services/execution/executor.py +1351 -0
- package/server/services/execution/models.py +531 -0
- package/server/services/execution/recovery.py +235 -0
- package/server/services/handlers/__init__.py +126 -0
- package/server/services/handlers/ai.py +355 -0
- package/server/services/handlers/android.py +260 -0
- package/server/services/handlers/code.py +278 -0
- package/server/services/handlers/document.py +598 -0
- package/server/services/handlers/http.py +193 -0
- package/server/services/handlers/polyglot.py +105 -0
- package/server/services/handlers/tools.py +845 -0
- package/server/services/handlers/triggers.py +107 -0
- package/server/services/handlers/utility.py +822 -0
- package/server/services/handlers/whatsapp.py +476 -0
- package/server/services/maps.py +289 -0
- package/server/services/memory_store.py +103 -0
- package/server/services/node_executor.py +375 -0
- package/server/services/parameter_resolver.py +218 -0
- package/server/services/polyglot_client.py +169 -0
- package/server/services/scheduler.py +155 -0
- package/server/services/skill_loader.py +417 -0
- package/server/services/status_broadcaster.py +826 -0
- package/server/services/temporal/__init__.py +23 -0
- package/server/services/temporal/activities.py +344 -0
- package/server/services/temporal/client.py +76 -0
- package/server/services/temporal/executor.py +147 -0
- package/server/services/temporal/worker.py +251 -0
- package/server/services/temporal/workflow.py +355 -0
- package/server/services/temporal/ws_client.py +236 -0
- package/server/services/text.py +111 -0
- package/server/services/user_auth.py +172 -0
- package/server/services/websocket_client.py +29 -0
- package/server/services/workflow.py +597 -0
- package/server/skills/android-skill/SKILL.md +82 -0
- package/server/skills/assistant-personality/SKILL.md +45 -0
- package/server/skills/code-skill/SKILL.md +140 -0
- package/server/skills/http-skill/SKILL.md +161 -0
- package/server/skills/maps-skill/SKILL.md +170 -0
- package/server/skills/memory-skill/SKILL.md +154 -0
- package/server/skills/scheduler-skill/SKILL.md +84 -0
- package/server/skills/whatsapp-skill/SKILL.md +283 -0
- package/server/uv.lock +2916 -0
- package/server/whatsapp-rpc/.dockerignore +30 -0
- package/server/whatsapp-rpc/Dockerfile +44 -0
- package/server/whatsapp-rpc/Dockerfile.web +17 -0
- package/server/whatsapp-rpc/README.md +139 -0
- package/server/whatsapp-rpc/cli.js +95 -0
- package/server/whatsapp-rpc/configs/config.yaml +7 -0
- package/server/whatsapp-rpc/docker-compose.yml +35 -0
- package/server/whatsapp-rpc/docs/API.md +410 -0
- package/server/whatsapp-rpc/go.mod +67 -0
- package/server/whatsapp-rpc/go.sum +203 -0
- package/server/whatsapp-rpc/package.json +30 -0
- package/server/whatsapp-rpc/schema.json +1294 -0
- package/server/whatsapp-rpc/scripts/clean.cjs +66 -0
- package/server/whatsapp-rpc/scripts/cli.js +162 -0
- package/server/whatsapp-rpc/src/go/cmd/server/main.go +91 -0
- package/server/whatsapp-rpc/src/go/config/config.go +49 -0
- package/server/whatsapp-rpc/src/go/rpc/rpc.go +446 -0
- package/server/whatsapp-rpc/src/go/rpc/server.go +112 -0
- package/server/whatsapp-rpc/src/go/whatsapp/history.go +166 -0
- package/server/whatsapp-rpc/src/go/whatsapp/messages.go +390 -0
- package/server/whatsapp-rpc/src/go/whatsapp/service.go +2130 -0
- package/server/whatsapp-rpc/src/go/whatsapp/types.go +261 -0
- package/server/whatsapp-rpc/src/python/pyproject.toml +15 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/__init__.py +4 -0
- package/server/whatsapp-rpc/src/python/whatsapp_rpc/client.py +427 -0
- package/server/whatsapp-rpc/web/app.py +609 -0
- package/server/whatsapp-rpc/web/requirements.txt +6 -0
- package/server/whatsapp-rpc/web/rpc_client.py +427 -0
- package/server/whatsapp-rpc/web/static/openapi.yaml +59 -0
- package/server/whatsapp-rpc/web/templates/base.html +150 -0
- package/server/whatsapp-rpc/web/templates/contacts.html +240 -0
- package/server/whatsapp-rpc/web/templates/dashboard.html +320 -0
- package/server/whatsapp-rpc/web/templates/groups.html +328 -0
- package/server/whatsapp-rpc/web/templates/messages.html +465 -0
- package/server/whatsapp-rpc/web/templates/messaging.html +681 -0
- package/server/whatsapp-rpc/web/templates/send.html +259 -0
- package/server/whatsapp-rpc/web/templates/settings.html +459 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Modern structured logging configuration with WebSocket broadcasting."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import asyncio
|
|
5
|
+
import structlog
|
|
6
|
+
import logging
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
from queue import Queue
|
|
11
|
+
from threading import Thread
|
|
12
|
+
from core.config import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WebSocketLogHandler(logging.Handler):
|
|
16
|
+
"""Logging handler that broadcasts logs to WebSocket clients.
|
|
17
|
+
|
|
18
|
+
Uses a thread-safe queue to bridge sync logging with async WebSocket broadcasting.
|
|
19
|
+
A background thread processes the queue and uses asyncio to broadcast.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
_instance: Optional['WebSocketLogHandler'] = None
|
|
23
|
+
|
|
24
|
+
def __init__(self, level: int = logging.INFO):
|
|
25
|
+
super().__init__(level)
|
|
26
|
+
self._queue: Queue = Queue(maxsize=1000) # Bounded queue to prevent memory issues
|
|
27
|
+
self._running = False
|
|
28
|
+
self._thread: Optional[Thread] = None
|
|
29
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
30
|
+
|
|
31
|
+
# Source name mapping for cleaner display
|
|
32
|
+
self._source_map = {
|
|
33
|
+
'services.workflow': 'workflow',
|
|
34
|
+
'services.ai': 'ai',
|
|
35
|
+
'services.android': 'android',
|
|
36
|
+
'routers.whatsapp': 'whatsapp',
|
|
37
|
+
'routers.android': 'android',
|
|
38
|
+
'routers.websocket': 'websocket',
|
|
39
|
+
'routers.workflow': 'workflow',
|
|
40
|
+
'services.execution': 'execution',
|
|
41
|
+
'services.deployment': 'deployment',
|
|
42
|
+
'__main__': 'main',
|
|
43
|
+
'main': 'main',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_instance(cls) -> Optional['WebSocketLogHandler']:
|
|
48
|
+
"""Get the singleton instance."""
|
|
49
|
+
return cls._instance
|
|
50
|
+
|
|
51
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
52
|
+
"""Queue log record for async broadcasting."""
|
|
53
|
+
if not self._running:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# Get the raw message without structlog formatting
|
|
58
|
+
message = record.getMessage()
|
|
59
|
+
|
|
60
|
+
# Map source name
|
|
61
|
+
source = record.name
|
|
62
|
+
for prefix, mapped in self._source_map.items():
|
|
63
|
+
if source.startswith(prefix):
|
|
64
|
+
source = mapped
|
|
65
|
+
break
|
|
66
|
+
|
|
67
|
+
# Extract structured key-value pairs from structlog
|
|
68
|
+
details = None
|
|
69
|
+
if hasattr(record, '_logger') or hasattr(record, 'positional_args'):
|
|
70
|
+
# Try to get extra kwargs from structlog
|
|
71
|
+
extra_keys = set(record.__dict__.keys()) - {
|
|
72
|
+
'name', 'msg', 'args', 'created', 'filename', 'funcName',
|
|
73
|
+
'levelname', 'levelno', 'lineno', 'module', 'msecs',
|
|
74
|
+
'pathname', 'process', 'processName', 'relativeCreated',
|
|
75
|
+
'stack_info', 'exc_info', 'exc_text', 'thread', 'threadName',
|
|
76
|
+
'message', 'asctime', 'positional_args', '_logger'
|
|
77
|
+
}
|
|
78
|
+
if extra_keys:
|
|
79
|
+
details = {k: record.__dict__[k] for k in extra_keys if not k.startswith('_')}
|
|
80
|
+
|
|
81
|
+
# Create log entry
|
|
82
|
+
log_data = {
|
|
83
|
+
'timestamp': datetime.now().isoformat(),
|
|
84
|
+
'level': record.levelname.lower(),
|
|
85
|
+
'message': message,
|
|
86
|
+
'source': source,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
# Add details if present
|
|
90
|
+
if details:
|
|
91
|
+
log_data['details'] = details
|
|
92
|
+
|
|
93
|
+
# Non-blocking put - drop if queue is full
|
|
94
|
+
try:
|
|
95
|
+
self._queue.put_nowait(log_data)
|
|
96
|
+
except:
|
|
97
|
+
pass # Drop log if queue is full
|
|
98
|
+
|
|
99
|
+
except Exception:
|
|
100
|
+
pass # Never fail in log handler
|
|
101
|
+
|
|
102
|
+
def start(self, loop: asyncio.AbstractEventLoop) -> None:
|
|
103
|
+
"""Start the background thread for processing logs."""
|
|
104
|
+
if self._running:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
self._loop = loop
|
|
108
|
+
self._running = True
|
|
109
|
+
self._thread = Thread(target=self._process_queue, daemon=True)
|
|
110
|
+
self._thread.start()
|
|
111
|
+
WebSocketLogHandler._instance = self
|
|
112
|
+
|
|
113
|
+
def stop(self) -> None:
|
|
114
|
+
"""Stop the background thread."""
|
|
115
|
+
self._running = False
|
|
116
|
+
WebSocketLogHandler._instance = None
|
|
117
|
+
if self._thread:
|
|
118
|
+
self._thread.join(timeout=1.0)
|
|
119
|
+
|
|
120
|
+
def _process_queue(self) -> None:
|
|
121
|
+
"""Background thread that processes log queue and broadcasts."""
|
|
122
|
+
while self._running:
|
|
123
|
+
try:
|
|
124
|
+
# Block for up to 0.1 seconds waiting for logs
|
|
125
|
+
try:
|
|
126
|
+
log_data = self._queue.get(timeout=0.1)
|
|
127
|
+
except:
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Schedule async broadcast on the event loop
|
|
131
|
+
if self._loop and self._running:
|
|
132
|
+
asyncio.run_coroutine_threadsafe(
|
|
133
|
+
self._broadcast(log_data),
|
|
134
|
+
self._loop
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
except Exception:
|
|
138
|
+
pass # Never fail in background thread
|
|
139
|
+
|
|
140
|
+
async def _broadcast(self, log_data: Dict[str, Any]) -> None:
|
|
141
|
+
"""Broadcast log to WebSocket clients."""
|
|
142
|
+
try:
|
|
143
|
+
from services.status_broadcaster import get_status_broadcaster
|
|
144
|
+
broadcaster = get_status_broadcaster()
|
|
145
|
+
await broadcaster.broadcast_terminal_log(log_data)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass # Don't fail if broadcaster not ready
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def configure_logging(settings: Settings) -> None:
|
|
151
|
+
"""Configure structured logging based on settings."""
|
|
152
|
+
|
|
153
|
+
# Set up log file if specified
|
|
154
|
+
if settings.log_file:
|
|
155
|
+
log_path = Path(settings.log_file)
|
|
156
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
157
|
+
|
|
158
|
+
# Configure file handler
|
|
159
|
+
file_handler = logging.FileHandler(log_path)
|
|
160
|
+
file_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
|
161
|
+
|
|
162
|
+
# Configure console handler
|
|
163
|
+
console_handler = logging.StreamHandler(sys.stdout)
|
|
164
|
+
console_handler.setLevel(getattr(logging, settings.log_level.upper()))
|
|
165
|
+
|
|
166
|
+
# Configure root logger
|
|
167
|
+
logging.basicConfig(
|
|
168
|
+
level=getattr(logging, settings.log_level.upper()),
|
|
169
|
+
handlers=[console_handler, file_handler],
|
|
170
|
+
format="%(message)s"
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
# Console only
|
|
174
|
+
logging.basicConfig(
|
|
175
|
+
format="%(message)s",
|
|
176
|
+
stream=sys.stdout,
|
|
177
|
+
level=getattr(logging, settings.log_level.upper())
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Configure structlog
|
|
181
|
+
processors = [
|
|
182
|
+
structlog.stdlib.filter_by_level,
|
|
183
|
+
structlog.stdlib.add_log_level,
|
|
184
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
185
|
+
structlog.processors.StackInfoRenderer(),
|
|
186
|
+
structlog.processors.format_exc_info,
|
|
187
|
+
structlog.processors.UnicodeDecoder(),
|
|
188
|
+
]
|
|
189
|
+
|
|
190
|
+
# Add appropriate renderer based on format
|
|
191
|
+
if settings.log_format == "json":
|
|
192
|
+
processors.insert(0, structlog.processors.TimeStamper(fmt="iso"))
|
|
193
|
+
processors.insert(0, structlog.stdlib.add_logger_name)
|
|
194
|
+
processors.append(structlog.processors.JSONRenderer())
|
|
195
|
+
else:
|
|
196
|
+
# Console format with timestamp
|
|
197
|
+
processors.insert(0, structlog.processors.TimeStamper(fmt="%H:%M:%S"))
|
|
198
|
+
processors.append(structlog.dev.ConsoleRenderer(
|
|
199
|
+
colors=False, # No ANSI colors for cleaner output
|
|
200
|
+
pad_event=35,
|
|
201
|
+
exception_formatter=structlog.dev.plain_traceback
|
|
202
|
+
))
|
|
203
|
+
|
|
204
|
+
structlog.configure(
|
|
205
|
+
processors=processors,
|
|
206
|
+
context_class=dict,
|
|
207
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
208
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
209
|
+
cache_logger_on_first_use=True,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def get_logger(name: str) -> structlog.BoundLogger:
|
|
214
|
+
"""Get a structured logger instance."""
|
|
215
|
+
return structlog.get_logger(name)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def log_execution_time(logger: structlog.BoundLogger, operation: str,
|
|
219
|
+
start_time: float, end_time: float, **kwargs) -> None:
|
|
220
|
+
"""Log execution time with additional context."""
|
|
221
|
+
execution_time = end_time - start_time
|
|
222
|
+
logger.info(
|
|
223
|
+
"Operation completed",
|
|
224
|
+
operation=operation,
|
|
225
|
+
execution_time_seconds=round(execution_time, 4),
|
|
226
|
+
**kwargs
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def log_api_call(logger: structlog.BoundLogger, provider: str, model: str,
|
|
231
|
+
operation: str, success: bool, **kwargs) -> None:
|
|
232
|
+
"""Log API calls with standardized format."""
|
|
233
|
+
logger.info(
|
|
234
|
+
"API call completed",
|
|
235
|
+
provider=provider,
|
|
236
|
+
model=model,
|
|
237
|
+
operation=operation,
|
|
238
|
+
success=success,
|
|
239
|
+
**kwargs
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def log_cache_operation(logger: structlog.BoundLogger, operation: str,
|
|
244
|
+
key: str, hit: bool = None, **kwargs) -> None:
|
|
245
|
+
"""Log cache operations."""
|
|
246
|
+
log_data = {
|
|
247
|
+
"operation": operation,
|
|
248
|
+
"cache_key": key,
|
|
249
|
+
**kwargs
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if hit is not None:
|
|
253
|
+
log_data["cache_hit"] = hit
|
|
254
|
+
|
|
255
|
+
logger.debug("Cache operation", **log_data)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# Global WebSocket log handler instance
|
|
259
|
+
_ws_log_handler: Optional[WebSocketLogHandler] = None
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def setup_websocket_logging(loop: asyncio.AbstractEventLoop, level: int = logging.INFO) -> WebSocketLogHandler:
|
|
263
|
+
"""Setup and start the WebSocket log handler.
|
|
264
|
+
|
|
265
|
+
Should be called during application startup after the event loop is running.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
loop: The asyncio event loop to use for broadcasting
|
|
269
|
+
level: Minimum log level to broadcast (default: INFO)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
The WebSocket log handler instance
|
|
273
|
+
"""
|
|
274
|
+
global _ws_log_handler
|
|
275
|
+
|
|
276
|
+
if _ws_log_handler is not None:
|
|
277
|
+
return _ws_log_handler
|
|
278
|
+
|
|
279
|
+
# Create handler
|
|
280
|
+
_ws_log_handler = WebSocketLogHandler(level=level)
|
|
281
|
+
|
|
282
|
+
# Add to root logger
|
|
283
|
+
root_logger = logging.getLogger()
|
|
284
|
+
root_logger.addHandler(_ws_log_handler)
|
|
285
|
+
|
|
286
|
+
# Start the background processing thread
|
|
287
|
+
_ws_log_handler.start(loop)
|
|
288
|
+
|
|
289
|
+
return _ws_log_handler
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def shutdown_websocket_logging() -> None:
|
|
293
|
+
"""Shutdown the WebSocket log handler.
|
|
294
|
+
|
|
295
|
+
Should be called during application shutdown.
|
|
296
|
+
"""
|
|
297
|
+
global _ws_log_handler
|
|
298
|
+
|
|
299
|
+
if _ws_log_handler is None:
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
# Stop the handler
|
|
303
|
+
_ws_log_handler.stop()
|
|
304
|
+
|
|
305
|
+
# Remove from root logger
|
|
306
|
+
root_logger = logging.getLogger()
|
|
307
|
+
root_logger.removeHandler(_ws_log_handler)
|
|
308
|
+
|
|
309
|
+
_ws_log_handler = None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_websocket_log_handler() -> Optional[WebSocketLogHandler]:
|
|
313
|
+
"""Get the current WebSocket log handler instance."""
|
|
314
|
+
return _ws_log_handler
|
package/server/main.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Modern FastAPI backend for React Flow workflow automation platform.
|
|
3
|
+
|
|
4
|
+
Refactored with dependency injection, modular services, and clean architecture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# Performance: Install uvloop if available (Linux/macOS only)
|
|
8
|
+
try:
|
|
9
|
+
import uvloop
|
|
10
|
+
uvloop.install()
|
|
11
|
+
except ImportError:
|
|
12
|
+
pass # Windows - uvloop not available, use default asyncio
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from contextlib import asynccontextmanager
|
|
17
|
+
|
|
18
|
+
# Note: We don't register custom signal handlers.
|
|
19
|
+
# uvicorn already handles SIGINT (Ctrl+C) and SIGTERM (docker stop) gracefully.
|
|
20
|
+
# Adding custom handlers that raise KeyboardInterrupt causes cascading errors
|
|
21
|
+
# during async operations (WebSocket handlers, logging, etc.).
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
25
|
+
from fastapi.responses import ORJSONResponse
|
|
26
|
+
|
|
27
|
+
from core.container import container
|
|
28
|
+
from core.config import Settings
|
|
29
|
+
from core.logging import configure_logging, get_logger, setup_websocket_logging, shutdown_websocket_logging
|
|
30
|
+
from routers import workflow, database, maps, nodejs_compat, android, websocket, webhook, auth
|
|
31
|
+
|
|
32
|
+
# Initialize settings and logging
|
|
33
|
+
settings = Settings()
|
|
34
|
+
configure_logging(settings)
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
# Suppress noisy loggers
|
|
38
|
+
import logging
|
|
39
|
+
logging.getLogger("apscheduler").setLevel(logging.WARNING)
|
|
40
|
+
logging.getLogger("apscheduler.scheduler").setLevel(logging.WARNING)
|
|
41
|
+
logging.getLogger("apscheduler.executors").setLevel(logging.WARNING)
|
|
42
|
+
logging.getLogger("uvicorn").setLevel(logging.WARNING)
|
|
43
|
+
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
|
|
44
|
+
logging.getLogger("uvicorn.error").setLevel(logging.WARNING)
|
|
45
|
+
logging.getLogger("watchfiles").setLevel(logging.WARNING)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@asynccontextmanager
|
|
49
|
+
async def lifespan(app: FastAPI):
|
|
50
|
+
"""Application lifespan management."""
|
|
51
|
+
# Startup
|
|
52
|
+
logger.info("Starting React Flow Python Services")
|
|
53
|
+
|
|
54
|
+
# Wire dependency injection
|
|
55
|
+
container.wire(modules=[
|
|
56
|
+
"routers.workflow",
|
|
57
|
+
"routers.database",
|
|
58
|
+
"routers.maps",
|
|
59
|
+
"routers.nodejs_compat",
|
|
60
|
+
"routers.android",
|
|
61
|
+
"routers.websocket",
|
|
62
|
+
"routers.webhook",
|
|
63
|
+
"routers.auth",
|
|
64
|
+
"middleware.auth"
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
# Start services
|
|
68
|
+
await container.database().startup()
|
|
69
|
+
await container.cache().startup()
|
|
70
|
+
|
|
71
|
+
# Initialize event waiter with cache service for Redis Streams support
|
|
72
|
+
from services import event_waiter
|
|
73
|
+
event_waiter.set_cache_service(container.cache())
|
|
74
|
+
|
|
75
|
+
# Start APScheduler for cron jobs
|
|
76
|
+
from services.scheduler import start_scheduler, shutdown_scheduler
|
|
77
|
+
start_scheduler()
|
|
78
|
+
|
|
79
|
+
# Initialize execution engine recovery sweeper
|
|
80
|
+
from services.execution import (
|
|
81
|
+
ExecutionCache,
|
|
82
|
+
RecoverySweeper,
|
|
83
|
+
set_recovery_sweeper,
|
|
84
|
+
)
|
|
85
|
+
execution_cache = ExecutionCache(container.cache())
|
|
86
|
+
recovery_sweeper = RecoverySweeper(execution_cache)
|
|
87
|
+
set_recovery_sweeper(recovery_sweeper)
|
|
88
|
+
|
|
89
|
+
# Scan for incomplete executions on startup
|
|
90
|
+
if settings.redis_enabled:
|
|
91
|
+
incomplete = await recovery_sweeper.scan_on_startup()
|
|
92
|
+
if incomplete:
|
|
93
|
+
logger.info("Found incomplete executions on startup",
|
|
94
|
+
count=len(incomplete),
|
|
95
|
+
execution_ids=incomplete)
|
|
96
|
+
|
|
97
|
+
# Start background recovery sweeper
|
|
98
|
+
await recovery_sweeper.start()
|
|
99
|
+
logger.info("Execution recovery sweeper started")
|
|
100
|
+
|
|
101
|
+
# Start WebSocket logging handler to broadcast logs to frontend
|
|
102
|
+
import asyncio
|
|
103
|
+
loop = asyncio.get_running_loop()
|
|
104
|
+
setup_websocket_logging(loop)
|
|
105
|
+
logger.info("WebSocket logging handler started")
|
|
106
|
+
|
|
107
|
+
# Initialize Temporal if enabled
|
|
108
|
+
temporal_worker_manager = None
|
|
109
|
+
if settings.temporal_enabled:
|
|
110
|
+
try:
|
|
111
|
+
from services.temporal import TemporalClientWrapper, TemporalExecutor
|
|
112
|
+
from services.temporal.worker import TemporalWorkerManager
|
|
113
|
+
|
|
114
|
+
logger.info(
|
|
115
|
+
"Initializing Temporal integration",
|
|
116
|
+
server_address=settings.temporal_server_address,
|
|
117
|
+
namespace=settings.temporal_namespace,
|
|
118
|
+
task_queue=settings.temporal_task_queue,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Connect Temporal client
|
|
122
|
+
temporal_client_wrapper = container.temporal_client()
|
|
123
|
+
temporal_client = await temporal_client_wrapper.connect()
|
|
124
|
+
|
|
125
|
+
# Create and set the Temporal executor on WorkflowService
|
|
126
|
+
temporal_executor = TemporalExecutor(
|
|
127
|
+
client=temporal_client,
|
|
128
|
+
task_queue=settings.temporal_task_queue,
|
|
129
|
+
)
|
|
130
|
+
container.workflow_service().set_temporal_executor(temporal_executor)
|
|
131
|
+
|
|
132
|
+
# Start embedded Temporal worker
|
|
133
|
+
temporal_worker_manager = TemporalWorkerManager(
|
|
134
|
+
client=temporal_client,
|
|
135
|
+
task_queue=settings.temporal_task_queue,
|
|
136
|
+
)
|
|
137
|
+
await temporal_worker_manager.start()
|
|
138
|
+
|
|
139
|
+
logger.info("Temporal integration initialized successfully")
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.error(f"Failed to initialize Temporal: {str(e)}")
|
|
143
|
+
logger.warning("Falling back to Redis/sequential execution")
|
|
144
|
+
|
|
145
|
+
logger.info("Services started successfully")
|
|
146
|
+
yield
|
|
147
|
+
|
|
148
|
+
# Shutdown
|
|
149
|
+
# Stop WebSocket logging handler
|
|
150
|
+
shutdown_websocket_logging()
|
|
151
|
+
|
|
152
|
+
# Stop Temporal worker if running
|
|
153
|
+
if temporal_worker_manager is not None:
|
|
154
|
+
await temporal_worker_manager.stop()
|
|
155
|
+
logger.info("Temporal worker stopped")
|
|
156
|
+
|
|
157
|
+
# Disconnect Temporal client if connected
|
|
158
|
+
if settings.temporal_enabled:
|
|
159
|
+
try:
|
|
160
|
+
await container.temporal_client().disconnect()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
# Close Android relay client (prevents "Unclosed client session" warning)
|
|
165
|
+
from services.android.manager import close_relay_client
|
|
166
|
+
await close_relay_client(clear_stored_session=False)
|
|
167
|
+
|
|
168
|
+
# Stop recovery sweeper first
|
|
169
|
+
if settings.redis_enabled:
|
|
170
|
+
await recovery_sweeper.stop()
|
|
171
|
+
logger.info("Execution recovery sweeper stopped")
|
|
172
|
+
|
|
173
|
+
shutdown_scheduler() # Stop APScheduler
|
|
174
|
+
await container.cache().shutdown()
|
|
175
|
+
await container.database().shutdown()
|
|
176
|
+
logger.info("Services shutdown complete")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# Create FastAPI app
|
|
180
|
+
app = FastAPI(
|
|
181
|
+
title="React Flow Python Services",
|
|
182
|
+
version="3.0.0",
|
|
183
|
+
description="Modern workflow automation backend with AI and Maps integration",
|
|
184
|
+
lifespan=lifespan,
|
|
185
|
+
default_response_class=ORJSONResponse
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Add exception handler middleware BEFORE CORS to catch all errors
|
|
189
|
+
from fastapi import Request, status
|
|
190
|
+
from fastapi.responses import JSONResponse
|
|
191
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
192
|
+
|
|
193
|
+
class CatchAllExceptionsMiddleware(BaseHTTPMiddleware):
|
|
194
|
+
async def dispatch(self, request: Request, call_next):
|
|
195
|
+
try:
|
|
196
|
+
return await call_next(request)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
import traceback
|
|
199
|
+
traceback.print_exc()
|
|
200
|
+
logger.error(f"Unhandled exception: {type(e).__name__}: {str(e)}", exc_info=True)
|
|
201
|
+
return JSONResponse(
|
|
202
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
203
|
+
content={
|
|
204
|
+
"success": False,
|
|
205
|
+
"error": f"{type(e).__name__}: {str(e)}",
|
|
206
|
+
"detail": "Internal server error"
|
|
207
|
+
}
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
app.add_middleware(CatchAllExceptionsMiddleware)
|
|
211
|
+
|
|
212
|
+
# Add Auth middleware (checks JWT cookie for protected routes)
|
|
213
|
+
from middleware.auth import AuthMiddleware
|
|
214
|
+
app.add_middleware(AuthMiddleware)
|
|
215
|
+
|
|
216
|
+
# Add CORS middleware (must be AFTER exception middleware)
|
|
217
|
+
logger.info("Configuring CORS middleware",
|
|
218
|
+
origins_count=len(settings.cors_origins),
|
|
219
|
+
origins=settings.cors_origins)
|
|
220
|
+
app.add_middleware(
|
|
221
|
+
CORSMiddleware,
|
|
222
|
+
allow_origins=settings.cors_origins,
|
|
223
|
+
allow_credentials=True,
|
|
224
|
+
allow_methods=["*"],
|
|
225
|
+
allow_headers=["*"],
|
|
226
|
+
expose_headers=["*"],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Include routers
|
|
230
|
+
app.include_router(auth.router) # Auth routes (login, register, logout, status)
|
|
231
|
+
app.include_router(nodejs_compat.router) # Node.js compatibility (includes root endpoints)
|
|
232
|
+
app.include_router(workflow.router)
|
|
233
|
+
app.include_router(database.router)
|
|
234
|
+
app.include_router(maps.router)
|
|
235
|
+
app.include_router(android.router)
|
|
236
|
+
app.include_router(websocket.router)
|
|
237
|
+
app.include_router(webhook.router)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.get("/health")
|
|
241
|
+
async def health_check():
|
|
242
|
+
"""Detailed health check."""
|
|
243
|
+
from services import event_waiter
|
|
244
|
+
from services.execution import get_recovery_sweeper
|
|
245
|
+
|
|
246
|
+
sweeper = get_recovery_sweeper()
|
|
247
|
+
|
|
248
|
+
# Check Temporal status
|
|
249
|
+
temporal_status = {
|
|
250
|
+
"enabled": settings.temporal_enabled,
|
|
251
|
+
"connected": False,
|
|
252
|
+
}
|
|
253
|
+
if settings.temporal_enabled:
|
|
254
|
+
try:
|
|
255
|
+
temporal_status["connected"] = container.temporal_client().is_connected
|
|
256
|
+
temporal_status["server_address"] = settings.temporal_server_address
|
|
257
|
+
temporal_status["task_queue"] = settings.temporal_task_queue
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
"status": "OK",
|
|
263
|
+
"service": "python",
|
|
264
|
+
"version": "3.2.0", # Bumped for Temporal integration
|
|
265
|
+
"environment": "development" if settings.debug else "production",
|
|
266
|
+
"redis_enabled": settings.redis_enabled,
|
|
267
|
+
"event_waiter_mode": event_waiter.get_backend_mode(),
|
|
268
|
+
"execution_engine": {
|
|
269
|
+
"enabled": settings.redis_enabled,
|
|
270
|
+
"recovery_sweeper": sweeper is not None and sweeper._running,
|
|
271
|
+
},
|
|
272
|
+
"temporal": temporal_status,
|
|
273
|
+
"timestamp": datetime.now().isoformat()
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
import uvicorn
|
|
279
|
+
logger.info("Starting React Flow Python Services",
|
|
280
|
+
host=settings.host, port=settings.port, debug=settings.debug)
|
|
281
|
+
uvicorn.run(
|
|
282
|
+
"main:app",
|
|
283
|
+
host=settings.host,
|
|
284
|
+
port=settings.port,
|
|
285
|
+
reload=settings.debug,
|
|
286
|
+
reload_dirs=["."] if settings.debug else None,
|
|
287
|
+
reload_excludes=["*.pyc", "__pycache__", "*.log", "*.db"] if settings.debug else None,
|
|
288
|
+
workers=1 if settings.debug else settings.workers
|
|
289
|
+
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Authentication middleware for route protection."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from fastapi import Request, HTTPException
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
7
|
+
|
|
8
|
+
from core.container import container
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Public routes that don't require authentication
|
|
13
|
+
PUBLIC_PATHS = frozenset([
|
|
14
|
+
"/health",
|
|
15
|
+
"/docs",
|
|
16
|
+
"/openapi.json",
|
|
17
|
+
"/redoc",
|
|
18
|
+
"/api/auth/status",
|
|
19
|
+
"/api/auth/login",
|
|
20
|
+
"/api/auth/register",
|
|
21
|
+
"/api/auth/logout",
|
|
22
|
+
"/ws/internal", # Internal WebSocket for Temporal workers
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
# Path prefixes that are public
|
|
26
|
+
PUBLIC_PREFIXES = (
|
|
27
|
+
"/webhook/",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class AuthMiddleware(BaseHTTPMiddleware):
|
|
32
|
+
"""Middleware to protect routes requiring authentication."""
|
|
33
|
+
|
|
34
|
+
async def dispatch(self, request: Request, call_next):
|
|
35
|
+
path = request.url.path
|
|
36
|
+
|
|
37
|
+
# Allow public paths
|
|
38
|
+
if self._is_public_path(path):
|
|
39
|
+
return await call_next(request)
|
|
40
|
+
|
|
41
|
+
# Get settings
|
|
42
|
+
settings = container.settings()
|
|
43
|
+
|
|
44
|
+
# Check if auth is disabled (VITE_AUTH_ENABLED=false)
|
|
45
|
+
if settings.vite_auth_enabled and settings.vite_auth_enabled.lower() == 'false':
|
|
46
|
+
# Auth disabled - set anonymous user and allow request
|
|
47
|
+
request.state.user_id = 0
|
|
48
|
+
request.state.user_email = 'anonymous'
|
|
49
|
+
request.state.is_owner = True
|
|
50
|
+
return await call_next(request)
|
|
51
|
+
|
|
52
|
+
# Auth enabled - check token
|
|
53
|
+
token = request.cookies.get(settings.jwt_cookie_name)
|
|
54
|
+
|
|
55
|
+
if not token:
|
|
56
|
+
return JSONResponse(
|
|
57
|
+
status_code=401,
|
|
58
|
+
content={"detail": "Not authenticated"}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Verify token
|
|
62
|
+
user_auth = container.user_auth_service()
|
|
63
|
+
payload = user_auth.verify_token(token)
|
|
64
|
+
|
|
65
|
+
if not payload:
|
|
66
|
+
return JSONResponse(
|
|
67
|
+
status_code=401,
|
|
68
|
+
content={"detail": "Invalid or expired session"}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Attach user info to request state for downstream handlers
|
|
72
|
+
request.state.user_id = payload.get("sub")
|
|
73
|
+
request.state.user_email = payload.get("email")
|
|
74
|
+
request.state.is_owner = payload.get("is_owner", False)
|
|
75
|
+
|
|
76
|
+
return await call_next(request)
|
|
77
|
+
|
|
78
|
+
def _is_public_path(self, path: str) -> bool:
|
|
79
|
+
"""Check if path is public (no auth required)."""
|
|
80
|
+
# Exact match
|
|
81
|
+
if path in PUBLIC_PATHS:
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Prefix match
|
|
85
|
+
for prefix in PUBLIC_PREFIXES:
|
|
86
|
+
if path.startswith(prefix):
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
return False
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Database models
|