Build Pigway Frame Flow with bilingual UI, complete KoKonna driver and safe automation
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
||||
from fastapi import Request, Security
|
||||
from fastapi.security import HTTPBearer
|
||||
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
from pigway_frame_flow.storage.database import encode, now_iso
|
||||
|
||||
bearer = HTTPBearer(auto_error=False)
|
||||
ALL_SCOPES = [
|
||||
"devices:read",
|
||||
"images:read",
|
||||
"images:delete",
|
||||
"display:write",
|
||||
"templates:read",
|
||||
"render:write",
|
||||
"jobs:read",
|
||||
"jobs:write",
|
||||
"history:read",
|
||||
"settings:write",
|
||||
"admin",
|
||||
]
|
||||
|
||||
|
||||
def seed_client(db, token):
|
||||
if token:
|
||||
digest = hashlib.sha256(token.encode()).hexdigest()
|
||||
db.execute(
|
||||
"INSERT INTO clients VALUES(?,?,?,?,?,?,?) ON CONFLICT(client_id) DO UPDATE SET token_hash=excluded.token_hash, enabled=1",
|
||||
("admin", digest, "Administrator", encode(ALL_SCOPES), now_iso(), None, 1),
|
||||
)
|
||||
else:
|
||||
db.execute("UPDATE clients SET enabled=0 WHERE client_id='admin'")
|
||||
|
||||
|
||||
def require(scope):
|
||||
def authorize(request: Request, credentials=Security(bearer)):
|
||||
db = request.app.state.service.db
|
||||
if not credentials:
|
||||
raise FlowError(E.UNAUTHORIZED, "Frame Flow API token required", 401)
|
||||
digest = hashlib.sha256(credentials.credentials.encode()).hexdigest()
|
||||
for row in db.all("SELECT * FROM clients WHERE enabled=1"):
|
||||
if hmac.compare_digest(digest, row["token_hash"]):
|
||||
permissions = json.loads(row["permissions"])
|
||||
if scope not in permissions and "admin" not in permissions:
|
||||
raise FlowError(
|
||||
E.FORBIDDEN, "Token scope does not allow this action", 403
|
||||
)
|
||||
request.state.client_id = row["client_id"]
|
||||
# Avoid writing last_used on every image or poll.
|
||||
if not row["last_used_at"] or (
|
||||
now_iso()[:16] != row["last_used_at"][:16]
|
||||
):
|
||||
db.execute(
|
||||
"UPDATE clients SET last_used_at=? WHERE client_id=?",
|
||||
(now_iso(), row["client_id"]),
|
||||
)
|
||||
return row["client_id"]
|
||||
raise FlowError(E.UNAUTHORIZED, "Invalid Frame Flow API token", 401)
|
||||
|
||||
return authorize
|
||||
@@ -0,0 +1,56 @@
|
||||
import yaml
|
||||
|
||||
from pigway_frame_flow.drivers.base import FrameDeviceDriver
|
||||
from pigway_frame_flow.drivers.kokonna.client import KokonnaClient
|
||||
from pigway_frame_flow.drivers.kokonna.driver import KokonnaDriver
|
||||
from pigway_frame_flow.drivers.kokonna.mock import MockClient
|
||||
from pigway_frame_flow.errors import ErrorCode as E
|
||||
from pigway_frame_flow.errors import FlowError
|
||||
|
||||
|
||||
class DeviceRegistry:
|
||||
def __init__(self, settings):
|
||||
self.settings = settings
|
||||
config = settings.config_dir / "devices.yml"
|
||||
devices = (
|
||||
yaml.safe_load(config.read_text())["devices"]
|
||||
if config.exists()
|
||||
else {
|
||||
settings.device_id: {
|
||||
"driver": "kokonna",
|
||||
"api_key_env": "KOKONNA_API_KEY",
|
||||
"enabled": True,
|
||||
}
|
||||
}
|
||||
)
|
||||
self.drivers: dict[str, FrameDeviceDriver] = {}
|
||||
self.keys = []
|
||||
for id, entry in devices.items():
|
||||
if not entry.get("enabled", True):
|
||||
continue
|
||||
if entry.get("driver") != "kokonna":
|
||||
raise ValueError("Unsupported configured device driver")
|
||||
if not __import__("re").fullmatch(r"[a-z0-9-]{1,60}", id):
|
||||
raise ValueError("Invalid configured device ID")
|
||||
client: MockClient | KokonnaClient
|
||||
if settings.mock:
|
||||
root = settings.data_dir / "cache" / id
|
||||
root.mkdir(parents=True, exist_ok=True)
|
||||
client = MockClient(root)
|
||||
else:
|
||||
from pigway_frame_flow.settings import secret
|
||||
|
||||
key = (
|
||||
settings.api_key
|
||||
if entry.get("api_key_env", "KOKONNA_API_KEY") == "KOKONNA_API_KEY"
|
||||
else secret(str(entry["api_key_env"]))
|
||||
)
|
||||
self.keys.append(key)
|
||||
settings.additional_secrets.append(key)
|
||||
client = KokonnaClient(key, settings.write_enabled)
|
||||
self.drivers[id] = KokonnaDriver(client, id, settings.sanitize)
|
||||
|
||||
def get(self, id: str) -> FrameDeviceDriver:
|
||||
if id not in self.drivers:
|
||||
raise FlowError(E.DEVICE_NOT_FOUND, "Device not found", 404)
|
||||
return self.drivers[id]
|
||||
Reference in New Issue
Block a user