1import logging
2from contextlib import asynccontextmanager
3from typing import AsyncGenerator
4
5from apscheduler.schedulers.asyncio import AsyncIOScheduler
6from fastapi import FastAPI, Request, status
7from fastapi.encoders import jsonable_encoder
8from fastapi.exceptions import RequestValidationError
9from fastapi.middleware.cors import CORSMiddleware
10from fastapi.responses import HTMLResponse, JSONResponse
11from fastapi_pagination import add_pagination
12from starlette.staticfiles import StaticFiles
13from uvicorn import Config, Server
14
15from app.config.env import (
16 DEBUG,
17 DOCS,
18 HOME_PAGE_TEMPLATE,
19 UVICORN_HOST,
20 UVICORN_PORT,
21 UVICORN_SSL_CERTFILE,
22 UVICORN_SSL_KEYFILE,
23 UVICORN_UDS,
24 DASHBOARD_PATH,
25)
26from app.templates import render_template
27from . import __version__
28from .routes import api_router
29from .tasks import (
30 nodes_startup,
31 record_user_usages,
32 reset_user_data_usage,
33 review_users,
34 expire_days_reached,
35)
36from .webhooks import webhooks_router
37
38logger = logging.getLogger(__name__)
39
40
41@asynccontextmanager
42async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
43 await nodes_startup()
44 yield
45 scheduler.shutdown()
46
47
48app = FastAPI(
49 title="MarzneshinAPI",
50 description="Unified GUI Censorship Resistant Solution Powered by Xray",
51 version=__version__,
52 lifespan=lifespan,
53 docs_url="/docs" if DOCS else None,
54 redoc_url="/redoc" if DOCS else None,
55)
56
57app.webhooks.include_router(webhooks_router)
58
59app.include_router(api_router)
60add_pagination(app)
61
62
63@app.get("/", response_class=HTMLResponse)
64def home_page():
65 return render_template(HOME_PAGE_TEMPLATE)
66
67
68app.add_middleware(
69 CORSMiddleware,
70 allow_origins=["*"],
71 allow_credentials=True,
72 allow_methods=["*"],
73 allow_headers=["*"],
74)
75
76scheduler = AsyncIOScheduler(timezone="UTC")
77scheduler.add_job(record_user_usages, "interval", coalesce=True, seconds=30)
78scheduler.add_job(
79 review_users, "interval", seconds=30, coalesce=True, max_instances=1
80)
81scheduler.add_job(
82 expire_days_reached, "interval", seconds=30, coalesce=True, max_instances=1
83)
84scheduler.add_job(reset_user_data_usage, "interval", coalesce=True, hours=1)
85
86
87@app.exception_handler(RequestValidationError)
88def validation_exception_handler(
89 request: Request, exc: RequestValidationError
90):
91 details = {}
92 for error in exc.errors():
93 details[error["loc"][-1]] = error.get("msg")
94 return JSONResponse(
95 status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
96 content=jsonable_encoder({"detail": details}),
97 )
98
99
100async def main():
101 if not DEBUG:
102 app.mount(
103 DASHBOARD_PATH,
104 StaticFiles(directory="dashboard/dist", html=True),
105 name="dashboard",
106 )
107 app.mount(
108 "/static/",
109 StaticFiles(directory="dashboard/dist/static"),
110 name="static",
111 )
112 app.mount(
113 "/locales/",
114 StaticFiles(directory="dashboard/dist/locales"),
115 name="locales",
116 )
117 scheduler.start()
118 cfg = Config(
119 app=app,
120 host=UVICORN_HOST,
121 port=UVICORN_PORT,
122 uds=(None if DEBUG else UVICORN_UDS),
123 ssl_certfile=UVICORN_SSL_CERTFILE,
124 ssl_keyfile=UVICORN_SSL_KEYFILE,
125 workers=1,
126 reload=DEBUG,
127 log_level=logging.DEBUG if DEBUG else logging.INFO,
128 )
129 server = Server(cfg)
130 await server.serve()