from contextlib import asynccontextmanager from pathlib import Path import logging from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from starlette.requests import Request from backend.database import init_db from backend.routers import api, admin from backend.config import get_settings from backend.bootstrap import ensure_self_signed_cert from backend.routers.admin import ( login, logout, check_auth, get_stats, list_servers, get_server, create_server, update_server, delete_server, list_domains, get_domain, create_domain, update_domain, delete_domain, list_logs, list_acme_configs, get_acme_config, create_acme_config, update_acme_config, delete_acme_config, issue_cert, renew_cert, auto_renew_all, list_acme_logs, get_cert_info, download_cert_file, admin_generate_script, ) logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger("certcenter") @asynccontextmanager async def lifespan(app: FastAPI): await init_db() settings = get_settings() ensure_self_signed_cert(settings.base_url, settings.cert_dir) logger.info("=== 服务启动完成 ===") # 打印所有已注册的路由 for route in app.routes: methods = getattr(route, "methods", None) path = getattr(route, "path", None) if path: logger.info(f" {methods} {path}") else: logger.info(f" [included router] {type(route).__name__}") yield app = FastAPI(title="CertCenter", version="1.0.0", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # ── 请求日志中间件 ── @app.middleware("http") async def log_request(request: Request, call_next): logger.info(f">>> {request.method} {request.url.path}") response = await call_next(request) logger.info(f"<<< {request.method} {request.url.path} -> {response.status_code}") return response # ── 客户端 API ── app.include_router(api.router, prefix="/api") # ── 管理 API(直接注册)── app.post("/admin/api/login")(login) app.post("/admin/api/logout")(logout) app.get("/admin/api/me")(check_auth) app.get("/admin/api/stats")(get_stats) app.get("/admin/api/servers")(list_servers) app.get("/admin/api/servers/{server_id}")(get_server) app.post("/admin/api/servers", status_code=201)(create_server) app.put("/admin/api/servers/{server_id}")(update_server) app.delete("/admin/api/servers/{server_id}")(delete_server) app.get("/admin/api/domains")(list_domains) app.get("/admin/api/domains/{domain_id}")(get_domain) app.post("/admin/api/domains", status_code=201)(create_domain) app.put("/admin/api/domains/{domain_id}")(update_domain) app.delete("/admin/api/domains/{domain_id}")(delete_domain) app.get("/admin/api/logs")(list_logs) app.get("/admin/api/acme/configs")(list_acme_configs) app.get("/admin/api/acme/configs/{config_id}")(get_acme_config) app.post("/admin/api/acme/configs")(create_acme_config) app.put("/admin/api/acme/configs/{config_id}")(update_acme_config) app.delete("/admin/api/acme/configs/{config_id}")(delete_acme_config) app.post("/admin/api/acme/issue/{domain_id}")(issue_cert) app.post("/admin/api/acme/renew/{domain_id}")(renew_cert) app.post("/admin/api/acme/auto-renew")(auto_renew_all) app.get("/admin/api/acme/logs")(list_acme_logs) app.get("/admin/api/cert-info/{domain_id}")(get_cert_info) app.get("/admin/api/cert-download/{domain_id}/{file_type}")(download_cert_file) app.get("/admin/api/script")(admin_generate_script) logger.info("=== 路由注册完成 ===") for route in app.routes: methods = getattr(route, "methods", None) path = getattr(route, "path", None) if path: logger.info(f" {methods} {path}") else: logger.info(f" [included router] {type(route).__name__}") # ── 前端静态文件 ── frontend_dist = Path(__file__).parent.parent / "frontend" / "dist" if frontend_dist.exists(): assets_dir = frontend_dist / "assets" if assets_dir.exists(): app.mount("/assets", StaticFiles(directory=str(assets_dir)), name="assets") index_file = frontend_dist / "index.html" @app.middleware("http") async def spa_fallback(request: Request, call_next): response = await call_next(request) if request.method == "GET" and response.status_code == 404: path = request.url.path if not path.startswith("/api/") and not path.startswith("/admin/api/") and not path.startswith("/assets/"): return FileResponse(str(index_file)) return response if __name__ == "__main__": import uvicorn uvicorn.run("backend.main:app", host="0.0.0.0", port=8021, reload=True)