kin: KIN-089 При попытке добавить креды прод сервера для проекта corelock вылетает 500 Internal Server Error

This commit is contained in:
Gros Frumos 2026-03-16 20:39:17 +02:00
parent e80e50ba0c
commit 4a65d90218
13 changed files with 1215 additions and 4 deletions

View file

@ -4,6 +4,7 @@ Run: uvicorn web.api:app --reload --port 8420
"""
import logging
import mimetypes
import shutil
import subprocess
import sys
@ -12,7 +13,7 @@ from pathlib import Path
# Ensure project root on sys.path
sys.path.insert(0, str(Path(__file__).parent.parent))
from fastapi import FastAPI, HTTPException, Query
from fastapi import FastAPI, File, HTTPException, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse, Response
from fastapi.staticfiles import StaticFiles
@ -1321,6 +1322,112 @@ def get_notifications(project_id: str | None = None):
return notifications
# ---------------------------------------------------------------------------
# Attachments (KIN-090)
# ---------------------------------------------------------------------------
_MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024 # 10 MB
def _attachment_dir(project_path: Path, task_id: str) -> Path:
"""Return (and create) {project_path}/.kin/attachments/{task_id}/."""
d = project_path / ".kin" / "attachments" / task_id
d.mkdir(parents=True, exist_ok=True)
return d
@app.post("/api/tasks/{task_id}/attachments", status_code=201)
async def upload_attachment(task_id: str, file: UploadFile = File(...)):
conn = get_conn()
t = models.get_task(conn, task_id)
if not t:
conn.close()
raise HTTPException(404, f"Task '{task_id}' not found")
p = models.get_project(conn, t["project_id"])
if not p or not p.get("path"):
conn.close()
raise HTTPException(400, "Attachments not supported for operations projects")
# Sanitize filename: strip directory components
safe_name = Path(file.filename or "upload").name
if not safe_name:
conn.close()
raise HTTPException(400, "Invalid filename")
att_dir = _attachment_dir(Path(p["path"]), task_id)
dest = att_dir / safe_name
# Path traversal guard
if not dest.is_relative_to(att_dir):
conn.close()
raise HTTPException(400, "Invalid filename")
# Read with size limit
content = await file.read(_MAX_ATTACHMENT_SIZE + 1)
if len(content) > _MAX_ATTACHMENT_SIZE:
conn.close()
raise HTTPException(413, f"File too large. Maximum size is {_MAX_ATTACHMENT_SIZE // (1024*1024)} MB")
dest.write_bytes(content)
mime_type = mimetypes.guess_type(safe_name)[0] or "application/octet-stream"
attachment = models.create_attachment(
conn, task_id,
filename=safe_name,
path=str(dest),
mime_type=mime_type,
size=len(content),
)
conn.close()
return JSONResponse(attachment, status_code=201)
@app.get("/api/tasks/{task_id}/attachments")
def list_task_attachments(task_id: str):
conn = get_conn()
t = models.get_task(conn, task_id)
if not t:
conn.close()
raise HTTPException(404, f"Task '{task_id}' not found")
attachments = models.list_attachments(conn, task_id)
conn.close()
return attachments
@app.delete("/api/tasks/{task_id}/attachments/{attachment_id}", status_code=204)
def delete_task_attachment(task_id: str, attachment_id: int):
conn = get_conn()
att = models.get_attachment(conn, attachment_id)
if not att or att["task_id"] != task_id:
conn.close()
raise HTTPException(404, f"Attachment #{attachment_id} not found")
# Delete file from disk
try:
Path(att["path"]).unlink(missing_ok=True)
except Exception:
pass
models.delete_attachment(conn, attachment_id)
conn.close()
return Response(status_code=204)
@app.get("/api/attachments/{attachment_id}/file")
def get_attachment_file(attachment_id: int):
conn = get_conn()
att = models.get_attachment(conn, attachment_id)
conn.close()
if not att:
raise HTTPException(404, f"Attachment #{attachment_id} not found")
file_path = Path(att["path"])
if not file_path.exists():
raise HTTPException(404, "Attachment file not found on disk")
return FileResponse(
str(file_path),
media_type=att["mime_type"],
filename=att["filename"],
)
# ---------------------------------------------------------------------------
# Chat (KIN-OBS-012)
# ---------------------------------------------------------------------------