__init__.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
  1. import asyncio
  2. import contextlib
  3. from contextlib import asynccontextmanager
  4. from pathlib import Path
  5. from fastapi import FastAPI, Request
  6. from fastapi.middleware.cors import CORSMiddleware
  7. from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
  8. from fastapi.staticfiles import StaticFiles
  9. from starlette.middleware.base import BaseHTTPMiddleware
  10. from room_cal.api._routes import services, calc, chat, credentials, prices, reservations, closed, discounts, actions, workflows, health, reload, sms, busy
  11. import os
  12. from room_cal.api._auth import router as auth_router, get_user, AUTH_DISABLED
  13. from room_cal._health import run_health_loop
  14. _VUE = Path(__file__).parents[1] / 'vue' / 'dist'
  15. _DEV_HTML = _VUE / 'dev.html'
  16. _CALENDAR_HTML = _VUE / 'calendar.html'
  17. _CALC_HTML = _VUE / 'calc.html'
  18. _API_BASE = os.getenv('APP_BASE_URL', '').rstrip('/')
  19. def _render(path: Path) -> str:
  20. return path.read_text().replace("var API_BASE = '';", f"var API_BASE = '{_API_BASE}';", 1)
  21. _HTML_PATHS = {'/dev', '/calendar', '/calc'}
  22. _PUBLIC_PREFIXES = ('/auth/', '/calc', '/health', '/busy')
  23. _OPEN_PATHS = {'/docs', '/openapi.json', '/redoc'}
  24. class _AuthMiddleware(BaseHTTPMiddleware):
  25. async def dispatch(self, request: Request, call_next):
  26. path = request.url.path
  27. if path in _OPEN_PATHS or any(path.startswith(p) for p in _PUBLIC_PREFIXES):
  28. return await call_next(request)
  29. user = get_user(request)
  30. if not user:
  31. if path in _HTML_PATHS:
  32. return RedirectResponse(f'/auth/login?next={path}')
  33. return JSONResponse({'detail': 'Not authenticated'}, status_code=401)
  34. request.state.user = user
  35. return await call_next(request)
  36. @asynccontextmanager
  37. async def _lifespan(app: FastAPI):
  38. task = asyncio.create_task(run_health_loop())
  39. yield
  40. task.cancel()
  41. with contextlib.suppress(asyncio.CancelledError):
  42. await task
  43. _log = __import__('logging').getLogger(__name__)
  44. def create_app() -> FastAPI:
  45. import sqlite3
  46. from caldav.lib.error import AuthorizationError
  47. app = FastAPI(title='room_cal', lifespan=_lifespan)
  48. @app.exception_handler(AuthorizationError)
  49. async def _caldav_auth_error(request: Request, exc: AuthorizationError):
  50. _log.error('caldav: %s', exc)
  51. return JSONResponse({'detail': str(exc)}, status_code=500)
  52. @app.exception_handler(sqlite3.OperationalError)
  53. async def _sqlite_error(request: Request, exc: sqlite3.OperationalError):
  54. _log.error('sqlite: %s', exc)
  55. return JSONResponse({'detail': str(exc)}, status_code=500)
  56. app.add_middleware(CORSMiddleware, allow_origins=['*'], allow_methods=['*'], allow_headers=['*'])
  57. app.add_middleware(_AuthMiddleware)
  58. app.include_router(auth_router)
  59. for mod in (services, calc, chat, credentials, prices, reservations, closed, discounts, actions, workflows, health, reload, sms, busy):
  60. app.include_router(mod.router)
  61. app.mount('/assets', StaticFiles(directory=str(_VUE / 'assets')), name='assets')
  62. @app.get('/dev', response_class=HTMLResponse, include_in_schema=False)
  63. async def dev_ui():
  64. return _render(_DEV_HTML)
  65. @app.get('/calendar', response_class=HTMLResponse, include_in_schema=False)
  66. async def calendar_ui():
  67. return _render(_CALENDAR_HTML)
  68. @app.get('/calc', response_class=HTMLResponse, include_in_schema=False)
  69. async def calc_ui():
  70. return _render(_CALC_HTML)
  71. return app