Add the web interface
Plain HTML, CSS and JavaScript modules, with no build step. Hash routes and relative URLs make it work under any path. The job list is polled: every 2 seconds while a job runs, and less often otherwise.
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
"""The Flask app: login, the JSON API and the static web interface."""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
@@ -8,6 +10,7 @@ import secrets
|
||||
import uuid
|
||||
from collections.abc import Callable, Iterable
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from flask import Flask, Response, g, jsonify, request, send_file, session
|
||||
@@ -397,6 +400,33 @@ def library_import() -> Any:
|
||||
return orchestrator.queue_import(books, bool(data.get("check", True)))
|
||||
|
||||
|
||||
# Web interface. It uses relative URLs and hash routes, so it works under any path.
|
||||
|
||||
|
||||
def _content_security_policy(html: str) -> str:
|
||||
"""Allow only Bindery's own files, and the inline scripts of the page by their hashes.
|
||||
|
||||
The trailing-slash redirect in index.html must be inline: before the
|
||||
redirect, a relative script URL misses the path prefix.
|
||||
"""
|
||||
hashes = [
|
||||
"'sha256-" + base64.b64encode(hashlib.sha256(script.encode()).digest()).decode() + "'"
|
||||
for script in re.findall(r"<script>(.*?)</script>", html, re.DOTALL)
|
||||
]
|
||||
return (
|
||||
f"default-src 'self'; script-src 'self' {' '.join(hashes)}; object-src 'none'; "
|
||||
"base-uri 'none'; form-action 'self'; frame-ancestors 'self'"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index() -> Any:
|
||||
response = app.send_static_file("index.html")
|
||||
html = Path(app.static_folder or "", "index.html").read_text(encoding="utf-8")
|
||||
response.headers["Content-Security-Policy"] = _content_security_policy(html)
|
||||
return response
|
||||
|
||||
|
||||
# Gunicorn runs one worker, and the dev server runs without the reloader, so this runs once.
|
||||
if config.START_THREADS:
|
||||
orchestrator.start_threads()
|
||||
|
||||
Reference in New Issue
Block a user