"""YakPanel - SSL/Domains API - Let's Encrypt via certbot""" import os from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from pydantic import BaseModel from app.core.database import get_db from app.core.config import get_runtime_config from app.core.utils import exec_shell_sync from app.api.auth import get_current_user from app.models.user import User from app.models.site import Site, Domain router = APIRouter(prefix="/ssl", tags=["ssl"]) @router.get("/domains") async def ssl_domains( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): """List all domains from sites with site path for certbot webroot""" result = await db.execute( select(Domain, Site).join(Site, Domain.pid == Site.id).order_by(Domain.name) ) rows = result.all() return [ { "id": d.id, "name": d.name, "port": d.port, "site_id": s.id, "site_name": s.name, "site_path": s.path, } for d, s in rows ] class RequestCertRequest(BaseModel): domain: str webroot: str email: str @router.post("/request") async def ssl_request_cert( body: RequestCertRequest, current_user: User = Depends(get_current_user), ): """Request Let's Encrypt certificate via certbot (webroot challenge)""" if not body.domain or not body.webroot or not body.email: raise HTTPException(status_code=400, detail="domain, webroot and email required") if ".." in body.domain or ".." in body.webroot: raise HTTPException(status_code=400, detail="Invalid path") cfg = get_runtime_config() allowed = [os.path.abspath(cfg["www_root"]), os.path.abspath(cfg["setup_path"])] webroot_abs = os.path.abspath(body.webroot) if not any(webroot_abs.startswith(a + os.sep) or webroot_abs == a for a in allowed): raise HTTPException(status_code=400, detail="Webroot must be under www_root or setup_path") cmd = ( f'certbot certonly --webroot -w "{body.webroot}" -d "{body.domain}" ' f'--non-interactive --agree-tos --email "{body.email}"' ) out, err = exec_shell_sync(cmd, timeout=120) if err and "error" in err.lower() and "successfully" not in err.lower(): raise HTTPException(status_code=500, detail=err.strip() or out.strip()) return {"status": True, "msg": "Certificate requested", "output": out} @router.get("/certificates") async def ssl_list_certificates(current_user: User = Depends(get_current_user)): """List existing Let's Encrypt certificates""" live_dir = "/etc/letsencrypt/live" if not os.path.isdir(live_dir): return {"certificates": []} certs = [] for name in os.listdir(live_dir): if name.startswith("."): continue path = os.path.join(live_dir, name) if os.path.isdir(path) and os.path.isfile(os.path.join(path, "fullchain.pem")): certs.append({"name": name, "path": path}) return {"certificates": sorted(certs, key=lambda x: x["name"])}