Files
factoryOps-v2/main.py
Johngreen 9f4583229a
All checks were successful
Deploy to Production / deploy (push) Successful in 1m11s
feat: replace mapping text input with company dropdown selector
- Add proxy endpoint GET /api/admin/digital-twin/companies
- Fetch company list from digital-twin API on modal open
- Select dropdown shows company name + industry type
- Remove manual UUID input in favor of selection
2026-02-12 15:18:52 +09:00

200 lines
5.9 KiB
Python

import logging
import os
from contextlib import asynccontextmanager
from typing import List, Optional
from dotenv import load_dotenv
load_dotenv()
from fastapi import FastAPI, Depends, HTTPException, Path
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from src.database.config import get_db, init_db
from src.auth.router import router as auth_router, admin_router as auth_admin_router
from src.auth.dependencies import require_auth, require_superadmin
from src.auth.models import TokenData
from src.auth.password import hash_password
from src.tenant import manager as tenant_manager
from src.tenant.manager import TenantNotFoundError, InvalidTenantIdError
from src.api.machines import router as machines_router
from src.api.equipment_sync import router as equipment_sync_router
from src.api.equipment_parts import router as equipment_parts_router
from src.api.templates import router as templates_router
from src.api.inspections import router as inspections_router
from src.api.alarms import router as alarms_router
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
await init_db()
logger.info("Database initialized")
yield
app = FastAPI(title="FactoryOps v2 API", lifespan=lifespan)
CORS_ORIGINS = (
os.getenv("CORS_ORIGINS", "").split(",") if os.getenv("CORS_ORIGINS") else []
)
if not CORS_ORIGINS:
CORS_ORIGINS = ["http://localhost:3100", "http://127.0.0.1:3100"]
app.add_middleware(
CORSMiddleware,
allow_origins=CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(auth_router)
app.include_router(auth_admin_router)
app.include_router(machines_router)
app.include_router(equipment_sync_router)
app.include_router(equipment_parts_router)
app.include_router(templates_router)
app.include_router(inspections_router)
app.include_router(alarms_router)
@app.get("/api/health")
async def health_check():
return {"status": "ok", "version": "2.0.0"}
class TenantCreate(BaseModel):
id: str
name: str
industry_type: str = "general"
class TenantDigitalTwinMapping(BaseModel):
digital_twin_company_id: Optional[str] = None
@app.get("/api/tenants")
async def list_accessible_tenants(
current_user: TokenData = Depends(require_auth),
db: AsyncSession = Depends(get_db),
):
all_tenants = await tenant_manager.list_tenants(db)
if current_user.role == "superadmin":
return {"tenants": [t for t in all_tenants if t.get("is_active", True)]}
if current_user.tenant_id:
return {
"tenants": [
t
for t in all_tenants
if t["id"] == current_user.tenant_id and t.get("is_active", True)
]
}
return {"tenants": []}
@app.get("/api/admin/tenants")
async def list_all_tenants(
current_user: TokenData = Depends(require_superadmin),
db: AsyncSession = Depends(get_db),
):
return {"tenants": await tenant_manager.list_tenants(db)}
@app.post("/api/admin/tenants")
async def create_tenant(
tenant: TenantCreate,
current_user: TokenData = Depends(require_superadmin),
db: AsyncSession = Depends(get_db),
):
try:
result = await tenant_manager.create_tenant(
db, tenant.id, tenant.name, tenant.industry_type
)
return {"status": "success", "tenant": result}
except InvalidTenantIdError as e:
raise HTTPException(status_code=400, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=409, detail=str(e))
@app.get(
"/api/admin/tenants/{tenant_id}",
)
async def get_tenant(
tenant_id: str = Path(..., pattern=r"^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"),
current_user: TokenData = Depends(require_superadmin),
db: AsyncSession = Depends(get_db),
):
try:
return await tenant_manager.get_tenant(db, tenant_id)
except TenantNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
@app.patch("/api/admin/tenants/{tenant_id}/digital-twin")
async def update_tenant_digital_twin_mapping(
body: TenantDigitalTwinMapping,
tenant_id: str = Path(..., pattern=r"^[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$"),
current_user: TokenData = Depends(require_superadmin),
db: AsyncSession = Depends(get_db),
):
try:
result = await tenant_manager.update_tenant(
db, tenant_id, digital_twin_company_id=body.digital_twin_company_id
)
return {"status": "success", "tenant": result}
except TenantNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e))
except InvalidTenantIdError as e:
raise HTTPException(status_code=400, detail=str(e))
@app.get("/api/admin/digital-twin/companies")
async def list_digital_twin_companies(
current_user: TokenData = Depends(require_superadmin),
):
import httpx
api_url = os.getenv("DIGITAL_TWIN_API_URL", "")
if not api_url:
raise HTTPException(
status_code=503, detail="DIGITAL_TWIN_API_URL not configured"
)
try:
async with httpx.AsyncClient(timeout=10.0) as client:
resp = await client.get(f"{api_url}/api/v1/companies")
resp.raise_for_status()
data = resp.json()
companies = data.get("data", [])
return {
"companies": [
{
"companyId": c.get("companyId"),
"name": c.get("name"),
"industryType": c.get("industryType"),
}
for c in companies
]
}
except httpx.HTTPError as e:
raise HTTPException(
status_code=502, detail=f"Failed to fetch from digital-twin: {e}"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"main:app",
host=os.getenv("HOST", "0.0.0.0"),
port=int(os.getenv("PORT", "8000")),
reload=True,
)