199 lines
6.9 KiB
Python
199 lines
6.9 KiB
Python
import asyncio
|
|
import os
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Depends, HTTPException, Header
|
|
|
|
from api.state import state, Status
|
|
from config import load as load_config
|
|
from output import list_transcripts, read_transcript
|
|
|
|
router = APIRouter()
|
|
_ws_clients: list[WebSocket] = []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auth dependency
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def current_user(authorization: Optional[str] = Header(None)) -> dict:
|
|
from auth import get_user_for_token
|
|
token = None
|
|
if authorization and authorization.startswith("Bearer "):
|
|
token = authorization[7:]
|
|
if not token:
|
|
raise HTTPException(status_code=401, detail="Nicht angemeldet")
|
|
user = get_user_for_token(token)
|
|
if not user:
|
|
raise HTTPException(status_code=401, detail="Ungültiger oder abgelaufener Token")
|
|
return user
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Auth endpoints (no current_user dependency — these are unauthenticated)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.post("/login")
|
|
async def login(body: dict):
|
|
from auth import authenticate
|
|
username = body.get("username", "")
|
|
password = body.get("password", "")
|
|
if not username or not password:
|
|
raise HTTPException(status_code=400, detail="Benutzername und Passwort erforderlich")
|
|
token = authenticate(username, password)
|
|
if not token:
|
|
raise HTTPException(status_code=401, detail="Ungültige Anmeldedaten")
|
|
return {"token": token, "username": username}
|
|
|
|
|
|
@router.post("/logout")
|
|
async def logout(authorization: Optional[str] = Header(None)):
|
|
from auth import invalidate_token
|
|
if authorization and authorization.startswith("Bearer "):
|
|
invalidate_token(authorization[7:])
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/setup")
|
|
async def setup_page():
|
|
from fastapi.responses import FileResponse
|
|
from auth import has_users
|
|
from pathlib import Path
|
|
if has_users():
|
|
from fastapi.responses import RedirectResponse
|
|
return RedirectResponse("/")
|
|
return FileResponse(str(Path(__file__).parent.parent / "frontend" / "setup.html"))
|
|
|
|
|
|
@router.post("/setup")
|
|
async def setup_post(body: dict):
|
|
from auth import has_users, create_user
|
|
from config import load as load_config
|
|
if has_users():
|
|
raise HTTPException(status_code=403, detail="Bereits eingerichtet")
|
|
username = body.get("username", "").strip()
|
|
password = body.get("password", "")
|
|
if not username or len(password) < 6:
|
|
raise HTTPException(status_code=400, detail="Ungültige Eingabe")
|
|
cfg = load_config()
|
|
default_dir = cfg["output"]["path"]
|
|
output_dir = body.get("output_dir") or default_dir
|
|
create_user(username, password, output_dir, is_admin=True)
|
|
return {"ok": True}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Protected endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/status")
|
|
async def get_status(user: dict = Depends(current_user)):
|
|
return {"status": state.status, "username": user["username"]}
|
|
|
|
|
|
@router.post("/toggle")
|
|
async def toggle_recording(user: dict = Depends(current_user)):
|
|
from api.pipeline import run_pipeline
|
|
if state.status == Status.RECORDING:
|
|
asyncio.create_task(run_pipeline())
|
|
return {"action": "stopped"}
|
|
if state.status == Status.ERROR:
|
|
await state.set_status(Status.IDLE)
|
|
return {"action": "reset"}
|
|
if state.status == Status.IDLE:
|
|
from audio import AudioRecorder
|
|
state._recorder = AudioRecorder()
|
|
state._recorder.start()
|
|
state.recording_user = user["username"]
|
|
state._recording_output_dir = os.path.join(user["output_dir"], user["username"])
|
|
state._recording_instructions = user.get("instructions", "")
|
|
await state.set_status(Status.RECORDING)
|
|
return {"action": "started"}
|
|
return {"action": "busy", "status": state.status}
|
|
|
|
|
|
@router.post("/instructions")
|
|
async def set_instructions(body: dict, user: dict = Depends(current_user)):
|
|
user["instructions"] = body.get("instructions", "")
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/transcripts")
|
|
async def get_transcripts(user: dict = Depends(current_user)):
|
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
|
return list_transcripts(user_dir)
|
|
|
|
|
|
@router.get("/transcripts/{filename}")
|
|
async def get_transcript(filename: str, user: dict = Depends(current_user)):
|
|
from fastapi.responses import PlainTextResponse
|
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
|
content = read_transcript(user_dir, filename)
|
|
if content is None:
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
return PlainTextResponse(content)
|
|
|
|
|
|
@router.delete("/transcripts/{filename}")
|
|
async def delete_transcript(filename: str, user: dict = Depends(current_user)):
|
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
|
if os.path.basename(filename) != filename or not filename.endswith(".md"):
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
path = os.path.join(user_dir, filename)
|
|
if not os.path.exists(path):
|
|
raise HTTPException(status_code=404, detail="Nicht gefunden")
|
|
os.unlink(path)
|
|
return {"ok": True}
|
|
|
|
|
|
@router.get("/config")
|
|
async def get_config(user: dict = Depends(current_user)):
|
|
return load_config()
|
|
|
|
|
|
@router.put("/config")
|
|
async def put_config(body: dict, user: dict = Depends(current_user)):
|
|
if not user.get("is_admin"):
|
|
raise HTTPException(status_code=403, detail="Nur Administratoren können die Config ändern")
|
|
cfg = load_config()
|
|
cfg.update(body)
|
|
return cfg
|
|
|
|
|
|
@router.post("/open")
|
|
async def open_file(body: dict, user: dict = Depends(current_user)):
|
|
import subprocess
|
|
path = body.get("path", "")
|
|
# Only allow opening files within the user's own output directory
|
|
user_dir = os.path.join(user["output_dir"], user["username"])
|
|
if path and os.path.exists(path) and os.path.abspath(path).startswith(os.path.abspath(user_dir)):
|
|
subprocess.Popen(["xdg-open", path])
|
|
return {"ok": True}
|
|
|
|
|
|
@router.websocket("/ws")
|
|
async def websocket_endpoint(ws: WebSocket):
|
|
from auth import get_user_for_token
|
|
token = ws.query_params.get("token", "")
|
|
user = get_user_for_token(token)
|
|
if not user:
|
|
await ws.close(code=4001)
|
|
return
|
|
await ws.accept()
|
|
_ws_clients.append(ws)
|
|
try:
|
|
while True:
|
|
await ws.receive_text()
|
|
except WebSocketDisconnect:
|
|
if ws in _ws_clients:
|
|
_ws_clients.remove(ws)
|
|
|
|
|
|
async def broadcast(message: dict):
|
|
for ws in list(_ws_clients):
|
|
try:
|
|
await ws.send_json(message)
|
|
except Exception:
|
|
if ws in _ws_clients:
|
|
_ws_clients.remove(ws)
|