feat: add boards and tasks management endpoints
This commit is contained in:
0
backend/app/core/__init__.py
Normal file
0
backend/app/core/__init__.py
Normal file
97
backend/app/core/auth.py
Normal file
97
backend/app/core/auth.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from functools import lru_cache
|
||||
from typing import Literal
|
||||
|
||||
from fastapi import Depends, HTTPException, Request, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer
|
||||
from fastapi_clerk_auth import HTTPAuthorizationCredentials as ClerkCredentials
|
||||
from pydantic import BaseModel, ValidationError
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.core.config import settings
|
||||
from app.db.session import get_session
|
||||
from app.models.users import User
|
||||
|
||||
security = HTTPBearer(auto_error=False)
|
||||
|
||||
|
||||
class ClerkTokenPayload(BaseModel):
|
||||
sub: str
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _build_clerk_http_bearer(auto_error: bool) -> ClerkHTTPBearer:
|
||||
if not settings.clerk_jwks_url:
|
||||
raise RuntimeError("CLERK_JWKS_URL is not set.")
|
||||
clerk_config = ClerkConfig(
|
||||
jwks_url=settings.clerk_jwks_url,
|
||||
verify_iat=settings.clerk_verify_iat,
|
||||
leeway=settings.clerk_leeway,
|
||||
)
|
||||
return ClerkHTTPBearer(config=clerk_config, auto_error=auto_error, add_state=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuthContext:
|
||||
actor_type: Literal["user"]
|
||||
user: User | None = None
|
||||
|
||||
|
||||
def _resolve_clerk_auth(
|
||||
request: Request, fallback: ClerkCredentials | None
|
||||
) -> ClerkCredentials | None:
|
||||
auth_data = getattr(request.state, "clerk_auth", None)
|
||||
return auth_data or fallback
|
||||
|
||||
|
||||
def _parse_subject(auth_data: ClerkCredentials | None) -> str | None:
|
||||
if not auth_data or not auth_data.decoded:
|
||||
return None
|
||||
payload = ClerkTokenPayload.model_validate(auth_data.decoded)
|
||||
return payload.sub
|
||||
|
||||
|
||||
async def get_auth_context(
|
||||
request: Request,
|
||||
credentials: HTTPAuthorizationCredentials | None = Depends(security),
|
||||
session: Session = Depends(get_session),
|
||||
) -> AuthContext:
|
||||
if credentials is None:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
try:
|
||||
guard = _build_clerk_http_bearer(auto_error=False)
|
||||
clerk_credentials = await guard(request)
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) from exc
|
||||
except HTTPException as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from exc
|
||||
|
||||
auth_data = _resolve_clerk_auth(request, clerk_credentials)
|
||||
try:
|
||||
clerk_user_id = _parse_subject(auth_data)
|
||||
except ValidationError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) from exc
|
||||
|
||||
if not clerk_user_id:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
user = session.exec(select(User).where(User.clerk_user_id == clerk_user_id)).first()
|
||||
if user is None:
|
||||
claims = auth_data.decoded if auth_data and auth_data.decoded else {}
|
||||
user = User(
|
||||
clerk_user_id=clerk_user_id,
|
||||
email=claims.get("email"),
|
||||
name=claims.get("name"),
|
||||
)
|
||||
session.add(user)
|
||||
session.commit()
|
||||
session.refresh(user)
|
||||
|
||||
return AuthContext(
|
||||
actor_type="user",
|
||||
user=user,
|
||||
)
|
||||
@@ -4,10 +4,29 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_file_encoding="utf-8",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
environment: str = "dev"
|
||||
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/openclaw_agency"
|
||||
redis_url: str = "redis://localhost:6379/0"
|
||||
|
||||
# Clerk auth (auth only; roles stored in DB)
|
||||
clerk_jwks_url: str = ""
|
||||
clerk_verify_iat: bool = True
|
||||
clerk_leeway: float = 10.0
|
||||
|
||||
# OpenClaw Gateway
|
||||
openclaw_gateway_url: str = ""
|
||||
openclaw_gateway_token: str = ""
|
||||
|
||||
database_url: str
|
||||
cors_origins: str = ""
|
||||
|
||||
# Database lifecycle
|
||||
db_auto_migrate: bool = False
|
||||
|
||||
settings = Settings() # type: ignore
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -2,59 +2,13 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _level() -> str:
|
||||
return (os.environ.get("LOG_LEVEL") or os.environ.get("UVICORN_LOG_LEVEL") or "INFO").upper()
|
||||
|
||||
|
||||
def configure_logging() -> None:
|
||||
"""Configure app logging to stream to stdout.
|
||||
|
||||
Uvicorn already logs requests, but we want our app/integrations logs to be visible
|
||||
in the same console stream.
|
||||
"""
|
||||
|
||||
level = getattr(logging, _level(), logging.INFO)
|
||||
|
||||
root = logging.getLogger()
|
||||
root.setLevel(level)
|
||||
|
||||
# Avoid duplicate handlers (e.g., when autoreload imports twice)
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in root.handlers):
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
handler.setLevel(level)
|
||||
formatter = logging.Formatter(
|
||||
fmt="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
|
||||
datefmt="%Y-%m-%dT%H:%M:%SZ",
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
root.addHandler(handler)
|
||||
|
||||
# Make common noisy loggers respect our level
|
||||
for name in [
|
||||
"uvicorn",
|
||||
"uvicorn.error",
|
||||
"uvicorn.access",
|
||||
"httpx",
|
||||
"requests",
|
||||
]:
|
||||
logging.getLogger(name).setLevel(level)
|
||||
|
||||
# Hide SQLAlchemy engine chatter unless explicitly debugging.
|
||||
# (You can still enable it by setting LOG_LEVEL=DEBUG and adjusting this.)
|
||||
logging.getLogger("sqlalchemy").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.pool").setLevel(logging.WARNING)
|
||||
logging.getLogger("sqlalchemy.dialects").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def log_kv(logger: logging.Logger, msg: str, **kv: Any) -> None:
|
||||
# Lightweight key-value logging without requiring JSON logging.
|
||||
if kv:
|
||||
suffix = " ".join(f"{k}={v!r}" for k, v in kv.items())
|
||||
logger.info(f"{msg} | {suffix}")
|
||||
else:
|
||||
logger.info(msg)
|
||||
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
level = logging._nameToLevel.get(level_name, logging.INFO)
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format="%(asctime)s %(levelname)s %(name)s %(message)s",
|
||||
force=True,
|
||||
)
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def public_api_base_url() -> str:
|
||||
"""Return a LAN-reachable base URL for the Mission Control API.
|
||||
|
||||
Priority:
|
||||
1) MISSION_CONTROL_BASE_URL env var (recommended)
|
||||
2) First non-loopback IPv4 from `hostname -I`
|
||||
|
||||
Never returns localhost because agents may run on another machine.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
explicit = os.environ.get("MISSION_CONTROL_BASE_URL")
|
||||
if explicit:
|
||||
return explicit.rstrip("/")
|
||||
|
||||
try:
|
||||
out = subprocess.check_output(["bash", "-lc", "hostname -I"], text=True).strip()
|
||||
ips = re.findall(r"\b(?:\d{1,3}\.){3}\d{1,3}\b", out)
|
||||
for ip in ips:
|
||||
if ip.startswith("127."):
|
||||
continue
|
||||
if ip.startswith("172.17."):
|
||||
continue
|
||||
if ip.startswith(("192.168.", "10.", "172.")):
|
||||
return f"http://{ip}:8000"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "http://<dev-machine-ip>:8000"
|
||||
Reference in New Issue
Block a user