Initial YakPanel commit

This commit is contained in:
Niranjan
2026-04-07 02:04:22 +05:30
commit 2826d3e7f3
5359 changed files with 1390724 additions and 0 deletions

View File

@@ -0,0 +1,127 @@
"""YakPanel - Plugin / Extensions API"""
import json
import re
from urllib.parse import urlparse
from urllib.request import urlopen, Request
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.api.auth import get_current_user
from app.models.user import User
from app.models.plugin import CustomPlugin
router = APIRouter(prefix="/plugin", tags=["plugin"])
# Built-in extensions (features) - always enabled
BUILTIN_PLUGINS = [
{"id": "backup", "name": "Backup", "version": "1.0", "desc": "Site and database backup/restore", "enabled": True, "builtin": True},
{"id": "ssl", "name": "SSL/ACME", "version": "1.0", "desc": "Let's Encrypt certificates", "enabled": True, "builtin": True},
{"id": "docker", "name": "Docker", "version": "1.0", "desc": "Container management", "enabled": True, "builtin": True},
{"id": "node", "name": "Node.js", "version": "1.0", "desc": "PM2 process manager", "enabled": True, "builtin": True},
{"id": "services", "name": "Services", "version": "1.0", "desc": "System service control", "enabled": True, "builtin": True},
{"id": "logs", "name": "Logs", "version": "1.0", "desc": "Log file viewer", "enabled": True, "builtin": True},
{"id": "terminal", "name": "Terminal", "version": "1.0", "desc": "Web terminal", "enabled": True, "builtin": True},
{"id": "monitor", "name": "Monitor", "version": "1.0", "desc": "System monitoring", "enabled": True, "builtin": True},
]
@router.get("/list")
async def plugin_list(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List built-in + custom plugins"""
result = await db.execute(select(CustomPlugin).order_by(CustomPlugin.id))
custom = result.scalars().all()
builtin_ids = {p["id"] for p in BUILTIN_PLUGINS}
plugins = list(BUILTIN_PLUGINS)
for c in custom:
plugins.append({
"id": c.plugin_id,
"name": c.name,
"version": c.version,
"desc": c.desc,
"enabled": c.enabled,
"builtin": False,
"db_id": c.id,
})
return {"plugins": plugins}
class AddPluginRequest(BaseModel):
url: str
@router.post("/add-from-url")
async def plugin_add_from_url(
body: AddPluginRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Add a third-party plugin from a JSON manifest URL"""
url = (body.url or "").strip()
if not url:
raise HTTPException(status_code=400, detail="URL required")
parsed = urlparse(url)
if parsed.scheme not in ("http", "https"):
raise HTTPException(status_code=400, detail="Only http/https URLs allowed")
if not parsed.netloc or parsed.netloc.startswith("127.") or parsed.netloc == "localhost":
raise HTTPException(status_code=400, detail="Invalid URL")
try:
req = Request(url, headers={"User-Agent": "YakPanel/1.0"})
with urlopen(req, timeout=10) as r:
data = r.read(64 * 1024).decode("utf-8", errors="replace")
except Exception as e:
raise HTTPException(status_code=400, detail=f"Failed to fetch: {str(e)[:100]}")
try:
manifest = json.loads(data)
except json.JSONDecodeError as e:
raise HTTPException(status_code=400, detail=f"Invalid JSON: {e}")
pid = (manifest.get("id") or "").strip()
name = (manifest.get("name") or "").strip()
if not pid or not name:
raise HTTPException(status_code=400, detail="Manifest must have 'id' and 'name'")
if not re.match(r"^[a-z0-9_-]+$", pid):
raise HTTPException(status_code=400, detail="Plugin id must be alphanumeric, underscore, hyphen only")
version = (manifest.get("version") or "1.0").strip()[:32]
desc = (manifest.get("desc") or "").strip()[:512]
# Check builtin conflict
if any(p["id"] == pid for p in BUILTIN_PLUGINS):
raise HTTPException(status_code=400, detail="Plugin id conflicts with built-in")
# Check existing custom
r = await db.execute(select(CustomPlugin).where(CustomPlugin.plugin_id == pid))
if r.scalar_one_or_none():
raise HTTPException(status_code=400, detail="Plugin already installed")
cp = CustomPlugin(
plugin_id=pid,
name=name,
version=version,
desc=desc,
source_url=url[:512],
enabled=True,
)
db.add(cp)
await db.commit()
return {"status": True, "msg": "Plugin added", "id": cp.plugin_id}
@router.delete("/{plugin_id}")
async def plugin_delete(
plugin_id: str,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Remove a custom plugin (built-in plugins cannot be removed)"""
if any(p["id"] == plugin_id for p in BUILTIN_PLUGINS):
raise HTTPException(status_code=400, detail="Cannot remove built-in plugin")
result = await db.execute(select(CustomPlugin).where(CustomPlugin.plugin_id == plugin_id))
cp = result.scalar_one_or_none()
if not cp:
raise HTTPException(status_code=404, detail="Plugin not found")
await db.delete(cp)
await db.commit()
return {"status": True, "msg": "Plugin removed"}