wp on webui
This commit is contained in:
30
Makefile
30
Makefile
@@ -1,4 +1,4 @@
|
||||
.PHONY: build run clean
|
||||
.PHONY: build run clean up down session try logs
|
||||
|
||||
IMAGE_NAME := opencode
|
||||
IMAGE_TAG := latest
|
||||
@@ -12,3 +12,31 @@ run:
|
||||
|
||||
clean:
|
||||
docker rmi $(IMAGE_NAME):$(IMAGE_TAG) || true
|
||||
|
||||
# Start the full stack (session-manager, docker-daemon, caddy)
|
||||
up:
|
||||
docker-compose up -d --build
|
||||
@echo "Stack started. Create a session with: make session"
|
||||
|
||||
# Stop the stack
|
||||
down:
|
||||
docker-compose down
|
||||
|
||||
# View logs
|
||||
logs:
|
||||
docker-compose logs -f session-manager
|
||||
|
||||
# Create a new session and display its info
|
||||
session:
|
||||
@echo "Creating new session..."
|
||||
@curl -s -X POST http://localhost:8080/sessions | jq .
|
||||
|
||||
# Try the web interface - creates a session and opens it
|
||||
try:
|
||||
@echo "Creating session and opening web interface..."
|
||||
@SESSION_ID=$$(curl -s -X POST http://localhost:8080/sessions | jq -r '.session_id') && \
|
||||
echo "Session created: $$SESSION_ID" && \
|
||||
echo "Opening http://localhost:8080/session/$$SESSION_ID" && \
|
||||
xdg-open "http://localhost:8080/session/$$SESSION_ID" 2>/dev/null || \
|
||||
open "http://localhost:8080/session/$$SESSION_ID" 2>/dev/null || \
|
||||
echo "Visit: http://localhost:8080/session/$$SESSION_ID"
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
"enabled": true
|
||||
},
|
||||
"lovdata": {
|
||||
"type": "http",
|
||||
"type": "remote",
|
||||
"url": "${MCP_SERVER}",
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,9 +14,9 @@ services:
|
||||
- ./session-manager/sessions:/app/sessions
|
||||
environment:
|
||||
# Docker TLS configuration
|
||||
- DOCKER_TLS_VERIFY=1
|
||||
- DOCKER_TLS_VERIFY=0
|
||||
- DOCKER_CERT_PATH=/etc/docker/certs
|
||||
- DOCKER_HOST=tcp://host.docker.internal:2376
|
||||
- DOCKER_HOST=http://docker-daemon:2375
|
||||
# Application configuration
|
||||
- MCP_SERVER=${MCP_SERVER:-http://localhost:8001}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
@@ -42,35 +42,30 @@ services:
|
||||
memory: 1G
|
||||
cpus: '1.0'
|
||||
|
||||
# Docker daemon with TLS enabled (must be configured separately)
|
||||
# Run: ./docker/scripts/setup-docker-tls.sh after generating certificates
|
||||
# Docker daemon (non-TLS for local development)
|
||||
# For production, use TLS with: ./docker/scripts/setup-docker-tls.sh
|
||||
docker-daemon:
|
||||
image: docker:dind
|
||||
privileged: true
|
||||
ports:
|
||||
- "${DOCKER_TLS_PORT:-2376}:2376"
|
||||
- "2375:2375"
|
||||
volumes:
|
||||
# Mount TLS certificates
|
||||
- ./docker/certs:/etc/docker/certs:ro
|
||||
# Mount daemon configuration
|
||||
- ./docker/daemon.json:/etc/docker/daemon.json:ro
|
||||
# Docker data persistence
|
||||
- docker-data:/var/lib/docker
|
||||
environment:
|
||||
- DOCKER_TLS_CERTDIR=/etc/docker/certs
|
||||
- DOCKER_TLS_CERTDIR=
|
||||
networks:
|
||||
- lovdata-network
|
||||
restart: unless-stopped
|
||||
# Only expose TLS port, not the socket
|
||||
command: --tlsverify --tlscacert=/etc/docker/certs/ca.pem --tlscert=/etc/docker/certs/server-cert.pem --tlskey=/etc/docker/certs/server-key.pem
|
||||
command: --host=tcp://0.0.0.0:2375 --host=unix:///var/run/docker.sock
|
||||
|
||||
# lovdata-mcp server is external - configured via MCP_SERVER environment variable
|
||||
|
||||
caddy:
|
||||
image: caddy:2.7-alpine
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
- "8080:80"
|
||||
- "8443:443"
|
||||
volumes:
|
||||
- ./nginx/Caddyfile:/etc/caddy/Caddyfile
|
||||
- caddy_data:/data
|
||||
@@ -81,10 +76,11 @@ services:
|
||||
|
||||
volumes:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
# Docker daemon data persistence
|
||||
caddy_config: # Docker daemon data persistence
|
||||
|
||||
docker-data:
|
||||
|
||||
|
||||
networks:
|
||||
lovdata-network:
|
||||
driver: bridge
|
||||
driver: bridge
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Lovdata Chat Reverse Proxy Configuration
|
||||
# Using HTTP for local development (no SSL warnings)
|
||||
|
||||
# Main web interface
|
||||
localhost {
|
||||
# Main web interface - HTTP only for development
|
||||
http://localhost {
|
||||
# API endpoints for session management
|
||||
handle /api/* {
|
||||
uri strip_prefix /api
|
||||
@@ -10,18 +11,54 @@ localhost {
|
||||
|
||||
# Session-specific routing - proxy to session manager for dynamic routing
|
||||
handle /session/{session_id}* {
|
||||
# Keep the full path including session ID for routing
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
# Static files and main interface
|
||||
handle /* {
|
||||
try_files {path} {path}/ /index.html
|
||||
file_server
|
||||
# OpenCode SPA runtime requests - route based on session cookie
|
||||
handle /global/* {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /assets/* {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /provider/* {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /provider {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /project {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /path {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /find/* {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /file {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
handle /file/* {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
# Health check
|
||||
handle /health {
|
||||
reverse_proxy session-manager:8000
|
||||
}
|
||||
|
||||
# Static files and main interface (fallback)
|
||||
handle /* {
|
||||
try_files {path} {path}/ /index.html
|
||||
file_server
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,15 @@ from typing import Dict, Optional, List, Any
|
||||
from contextlib import asynccontextmanager
|
||||
import os
|
||||
|
||||
from aiodeocker import Docker
|
||||
from aiodeocker.containers import DockerContainer
|
||||
from aiodeocker.exceptions import DockerError
|
||||
from aiodocker import Docker
|
||||
from aiodocker.containers import DockerContainer
|
||||
from aiodocker.exceptions import DockerError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsyncDockerClient:
|
||||
"""Async wrapper for Docker operations using aiodeocker."""
|
||||
"""Async wrapper for Docker operations using aiodocker."""
|
||||
|
||||
def __init__(self):
|
||||
self._docker: Optional[Docker] = None
|
||||
@@ -38,29 +38,27 @@ class AsyncDockerClient:
|
||||
return
|
||||
|
||||
try:
|
||||
# Configure TLS if available
|
||||
tls_config = None
|
||||
# Configure TLS/SSL context based on environment
|
||||
ssl_ctx = None
|
||||
if os.getenv("DOCKER_TLS_VERIFY") == "1":
|
||||
from aiodeocker.utils import create_tls_config
|
||||
|
||||
tls_config = create_tls_config(
|
||||
ca_cert=os.getenv("DOCKER_CA_CERT", "/etc/docker/certs/ca.pem"),
|
||||
client_cert=(
|
||||
os.getenv(
|
||||
"DOCKER_CLIENT_CERT", "/etc/docker/certs/client-cert.pem"
|
||||
),
|
||||
os.getenv(
|
||||
"DOCKER_CLIENT_KEY", "/etc/docker/certs/client-key.pem"
|
||||
),
|
||||
),
|
||||
verify=True,
|
||||
import ssl
|
||||
|
||||
ssl_ctx = ssl.create_default_context(
|
||||
cafile=os.getenv("DOCKER_CA_CERT", "/etc/docker/certs/ca.pem")
|
||||
)
|
||||
ssl_ctx.load_cert_chain(
|
||||
certfile=os.getenv("DOCKER_CLIENT_CERT", "/etc/docker/certs/client-cert.pem"),
|
||||
keyfile=os.getenv("DOCKER_CLIENT_KEY", "/etc/docker/certs/client-key.pem")
|
||||
)
|
||||
else:
|
||||
# Explicitly disable SSL using False
|
||||
ssl_ctx = False
|
||||
|
||||
docker_host = os.getenv("DOCKER_HOST", "tcp://host.docker.internal:2376")
|
||||
self._docker = Docker(docker_host, tls=tls_config)
|
||||
self._docker = Docker(docker_host, ssl_context=ssl_ctx)
|
||||
|
||||
# Test connection
|
||||
await self._docker.ping()
|
||||
# Test connection using version() since aiodocker doesn't have ping()
|
||||
await self._docker.version()
|
||||
self._connected = True
|
||||
logger.info("Async Docker client connected successfully")
|
||||
|
||||
@@ -80,7 +78,7 @@ class AsyncDockerClient:
|
||||
if not self._docker:
|
||||
return False
|
||||
try:
|
||||
await self._docker.ping()
|
||||
await self._docker.version()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -208,7 +208,7 @@ def setup_logging(
|
||||
# Configure third-party loggers
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("docker").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiodeocker").setLevel(logging.WARNING)
|
||||
logging.getLogger("aiodocker").setLevel(logging.WARNING)
|
||||
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
||||
|
||||
return adapter
|
||||
|
||||
@@ -6,6 +6,7 @@ Each session gets its own isolated container with a dedicated working directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import uuid
|
||||
import json
|
||||
import asyncio
|
||||
@@ -15,6 +16,7 @@ from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
from contextlib import asynccontextmanager
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import docker
|
||||
from docker.errors import DockerException, NotFound
|
||||
@@ -358,7 +360,7 @@ class SessionManager:
|
||||
return session
|
||||
|
||||
async def _start_container_async(self, session: SessionData):
|
||||
"""Start the OpenCode container asynchronously using aiodeocker"""
|
||||
"""Start the OpenCode container asynchronously using aiodocker"""
|
||||
try:
|
||||
# Get and validate resource limits
|
||||
resource_limits = get_resource_limits()
|
||||
@@ -378,8 +380,8 @@ class SessionManager:
|
||||
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
|
||||
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""),
|
||||
"GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""),
|
||||
# Secure authentication for OpenCode server
|
||||
"OPENCODE_SERVER_PASSWORD": session.auth_token or "",
|
||||
# Auth disabled for development - will be added later
|
||||
# "OPENCODE_SERVER_PASSWORD": session.auth_token or "",
|
||||
"SESSION_AUTH_TOKEN": session.auth_token or "",
|
||||
"SESSION_ID": session.session_id,
|
||||
},
|
||||
@@ -395,10 +397,8 @@ class SessionManager:
|
||||
},
|
||||
)
|
||||
|
||||
# For async mode, containers are already started during creation
|
||||
# For sync mode, we need to explicitly start them
|
||||
if not self.docker_service.use_async:
|
||||
await self.docker_service.start_container(container_info.container_id)
|
||||
# Containers need to be explicitly started after creation
|
||||
await self.docker_service.start_container(container_info.container_id)
|
||||
|
||||
session.container_id = container_info.container_id
|
||||
session.status = "running"
|
||||
@@ -458,8 +458,8 @@ class SessionManager:
|
||||
"OPENAI_API_KEY": os.getenv("OPENAI_API_KEY", ""),
|
||||
"ANTHROPIC_API_KEY": os.getenv("ANTHROPIC_API_KEY", ""),
|
||||
"GOOGLE_API_KEY": os.getenv("GOOGLE_API_KEY", ""),
|
||||
# Secure authentication for OpenCode server
|
||||
"OPENCODE_SERVER_PASSWORD": session.auth_token or "",
|
||||
# Auth disabled for development - will be added later
|
||||
# "OPENCODE_SERVER_PASSWORD": session.auth_token or "",
|
||||
"SESSION_AUTH_TOKEN": session.auth_token or "",
|
||||
"SESSION_ID": session.session_id,
|
||||
},
|
||||
@@ -883,6 +883,118 @@ async def get_session_container_health(session_id: str):
|
||||
}
|
||||
|
||||
|
||||
# Routes for OpenCode SPA runtime requests
|
||||
# These paths are hardcoded in the compiled JS and need cookie-based session routing
|
||||
@app.api_route(
|
||||
"/global/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_global_to_session(request: Request, path: str):
|
||||
"""Proxy /global/* requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
# Redirect to session-prefixed path
|
||||
return await proxy_to_session(request, session_id, f"global/{path}")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/assets/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_assets_to_session(request: Request, path: str):
|
||||
"""Proxy /assets/* requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
# Redirect to session-prefixed path
|
||||
return await proxy_to_session(request, session_id, f"assets/{path}")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/provider/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_provider_path_to_session(request: Request, path: str):
|
||||
"""Proxy /provider/* requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, f"provider/{path}")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/provider",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_provider_to_session(request: Request):
|
||||
"""Proxy /provider requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, "provider")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/project",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_project_to_session(request: Request):
|
||||
"""Proxy /project requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, "project")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/path",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_path_to_session(request: Request):
|
||||
"""Proxy /path requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, "path")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/find/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_find_to_session(request: Request, path: str):
|
||||
"""Proxy /find/* requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, f"find/{path}")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/file",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_file_to_session(request: Request):
|
||||
"""Proxy /file requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, "file")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/file/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
)
|
||||
async def proxy_file_path_to_session(request: Request, path: str):
|
||||
"""Proxy /file/* requests to the current session based on cookie"""
|
||||
session_id = request.cookies.get("lovdata_session")
|
||||
if not session_id:
|
||||
raise HTTPException(status_code=400, detail="No active session - please access via /session/{id}/ first")
|
||||
return await proxy_to_session(request, session_id, f"file/{path}")
|
||||
|
||||
|
||||
@app.api_route(
|
||||
"/session/{session_id}/{path:path}",
|
||||
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"],
|
||||
@@ -916,40 +1028,14 @@ async def proxy_to_session(request: Request, session_id: str, path: str):
|
||||
status_code=404, detail="Session not found or not running"
|
||||
)
|
||||
|
||||
# Dynamically detect the Docker host IP from container perspective
|
||||
# This supports multiple Docker environments (Docker Desktop, Linux, cloud, etc.)
|
||||
try:
|
||||
host_ip = await async_get_host_ip()
|
||||
logger.info(f"Using detected host IP for proxy: {host_ip}")
|
||||
except RuntimeError as e:
|
||||
# Fallback to environment variable or common defaults
|
||||
host_ip = os.getenv("HOST_IP")
|
||||
if not host_ip:
|
||||
# Try common Docker gateway IPs as final fallback
|
||||
common_gateways = ["172.17.0.1", "192.168.65.1", "host.docker.internal"]
|
||||
for gateway in common_gateways:
|
||||
try:
|
||||
# Test connectivity to gateway
|
||||
import socket
|
||||
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1.0)
|
||||
result = sock.connect_ex((gateway, 22))
|
||||
sock.close()
|
||||
if result == 0:
|
||||
host_ip = gateway
|
||||
logger.warning(f"Using fallback gateway IP: {host_ip}")
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Host IP detection failed: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Could not determine Docker host IP for proxy routing",
|
||||
)
|
||||
|
||||
container_url = f"http://{host_ip}:{session.port}"
|
||||
# For DinD architecture: containers run inside docker-daemon with ports mapped
|
||||
# to docker-daemon's interface, so we proxy through docker-daemon hostname
|
||||
docker_host = os.getenv("DOCKER_HOST", "http://docker-daemon:2375")
|
||||
# Extract hostname from DOCKER_HOST (e.g., "http://docker-daemon:2375" -> "docker-daemon")
|
||||
parsed = urlparse(docker_host)
|
||||
container_host = parsed.hostname or "docker-daemon"
|
||||
|
||||
container_url = f"http://{container_host}:{session.port}"
|
||||
|
||||
# Prepare the request URL
|
||||
url = f"{container_url}/{path}"
|
||||
@@ -1002,12 +1088,46 @@ async def proxy_to_session(request: Request, session_id: str, path: str):
|
||||
status_code=response.status_code,
|
||||
)
|
||||
|
||||
# Return the response
|
||||
return Response(
|
||||
content=response.content,
|
||||
# Return the response - inject base tag for HTML to fix asset paths
|
||||
content = response.content
|
||||
response_headers = dict(response.headers)
|
||||
content_type = response.headers.get("content-type", "")
|
||||
|
||||
# For HTML responses, rewrite root-relative paths to include session prefix
|
||||
# OpenCode uses paths like /assets/, /favicon.ico which bypass <base> tag
|
||||
# We need to prepend the session path to make them work
|
||||
if "text/html" in content_type:
|
||||
try:
|
||||
html = content.decode("utf-8")
|
||||
session_prefix = f"/session/{session_id}"
|
||||
|
||||
# Rewrite src="/..." to src="/session/{id}/..."
|
||||
html = re.sub(r'src="/', f'src="{session_prefix}/', html)
|
||||
# Rewrite href="/..." to href="/session/{id}/..."
|
||||
html = re.sub(r'href="/', f'href="{session_prefix}/', html)
|
||||
# Rewrite content="/..." for meta tags
|
||||
html = re.sub(r'content="/', f'content="{session_prefix}/', html)
|
||||
|
||||
content = html.encode("utf-8")
|
||||
response_headers["content-length"] = str(len(content))
|
||||
except UnicodeDecodeError:
|
||||
pass # Not valid UTF-8, skip rewriting
|
||||
|
||||
# Create response with session tracking cookie
|
||||
resp = Response(
|
||||
content=content,
|
||||
status_code=response.status_code,
|
||||
headers=dict(response.headers),
|
||||
headers=response_headers,
|
||||
)
|
||||
# Set cookie to track current session for /global/* and /assets/* routing
|
||||
resp.set_cookie(
|
||||
key="lovdata_session",
|
||||
value=session_id,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=86400, # 24 hours
|
||||
)
|
||||
return resp
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
duration_ms = (time.time() - start_time) * 1000
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
docker>=7.1.0
|
||||
aiodeocker>=0.21.0
|
||||
aiodocker>=0.21.0
|
||||
asyncpg>=0.29.0
|
||||
pydantic==2.5.0
|
||||
python-multipart==0.0.6
|
||||
|
||||
Reference in New Issue
Block a user