Docs: rewrite README with clear Docker and Docker Compose instructions; refactor app into package (auth, admin, pages, db); keep single entrypoint app.py

This commit is contained in:
2025-09-04 10:48:04 +03:00
parent cc767dcf46
commit f4e1bc8a95
8 changed files with 386 additions and 453 deletions

34
app/__init__.py Normal file
View File

@@ -0,0 +1,34 @@
import os
from flask import Flask
from . import db
from .auth import auth_bp
from .admin import admin_bp
from .pages import pages_bp
def create_app() -> Flask:
app = Flask(__name__)
# Basic config
app.config["SECRET_KEY"] = os.environ.get(
"SECRET_KEY",
"dev-secret-change-me",
)
app.config["DATABASE"] = os.environ.get(
"DATABASE",
os.path.join(os.path.dirname(os.path.dirname(__file__)), "app.db"),
)
app.config["ADMIN_USERNAME"] = os.environ.get("ADMIN_USERNAME", "ruslan")
app.config["ADMIN_PASSWORD"] = os.environ.get("ADMIN_PASSWORD", "utOgbZ09ruslan")
# Init DB hooks
db.init_app(app)
# Register blueprints
app.register_blueprint(auth_bp)
app.register_blueprint(admin_bp)
app.register_blueprint(pages_bp)
return app

73
app/admin.py Normal file
View File

@@ -0,0 +1,73 @@
import uuid as uuid_lib
from datetime import datetime
from flask import Blueprint, render_template, request, redirect, url_for, flash
from .db import get_db
from .auth import login_required
admin_bp = Blueprint("admin", __name__, url_prefix="/admin")
@admin_bp.route("/", methods=["GET", "POST"])
def index():
login_required()
db = get_db()
if request.method == "POST":
title = request.form.get("title", "").strip()
html = request.form.get("html", "").strip()
if not html:
flash("HTML не может быть пустым.", "error")
else:
uid = uuid_lib.uuid4().hex
db.execute(
"INSERT INTO pages (uuid, title, html, created_at) VALUES (?, ?, ?, ?)",
(uid, title, html, datetime.utcnow().isoformat(timespec="seconds")),
)
db.commit()
flash("Страница опубликована.", "success")
return redirect(url_for("admin.index"))
pages = db.execute(
"SELECT id, uuid, title, created_at FROM pages ORDER BY id DESC"
).fetchall()
from flask import request as _request # local import
base_url = _request.host_url.rstrip("/")
return render_template("admin.html", pages=pages, base_url=base_url)
@admin_bp.route("/edit/<int:pid>", methods=["GET", "POST"])
def edit(pid: int):
login_required()
db = get_db()
if request.method == "POST":
title = request.form.get("title", "").strip()
html = request.form.get("html", "").strip()
if not html:
flash("HTML не может быть пустым.", "error")
else:
db.execute("UPDATE pages SET title = ?, html = ? WHERE id = ?", (title, html, pid))
db.commit()
flash("Страница обновлена.", "success")
return redirect(url_for("admin.index"))
row = db.execute("SELECT id, uuid, title, html, created_at FROM pages WHERE id = ?", (pid,)).fetchone()
if row is None:
from flask import abort as _abort
_abort(404)
return render_template("edit.html", page=row)
@admin_bp.route("/delete/<int:pid>", methods=["POST"])
def delete(pid: int):
login_required()
db = get_db()
cur = db.execute("DELETE FROM pages WHERE id = ?", (pid,))
db.commit()
if cur.rowcount:
flash("Страница удалена.", "success")
else:
flash("Страница не найдена.", "error")
return redirect(url_for("admin.index"))

41
app/auth.py Normal file
View File

@@ -0,0 +1,41 @@
from flask import Blueprint, current_app, render_template, request, redirect, url_for, session, abort, flash
auth_bp = Blueprint("auth", __name__)
def is_logged_in() -> bool:
return bool(session.get("logged_in"))
def login_required():
if not is_logged_in():
abort(404)
@auth_bp.app_context_processor
def inject_auth():
return {"logged_in": is_logged_in()}
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
if request.method == "POST":
username = request.form.get("username", "")
password = request.form.get("password", "")
if (
username == current_app.config["ADMIN_USERNAME"]
and password == current_app.config["ADMIN_PASSWORD"]
):
session["logged_in"] = True
flash("Вход выполнен", "success")
return redirect(url_for("admin.index"))
flash("Неверные логин или пароль", "error")
return render_template("login.html")
@auth_bp.route("/logout", methods=["POST"])
def logout():
session.clear()
abort(404)

50
app/db.py Normal file
View File

@@ -0,0 +1,50 @@
import sqlite3
from datetime import datetime
from flask import g, current_app
def get_db():
if "db" not in g:
g.db = sqlite3.connect(current_app.config["DATABASE"]) # type: ignore[attr-defined]
g.db.row_factory = sqlite3.Row
return g.db
def close_db(_=None):
db = g.pop("db", None)
if db is not None:
db.close()
def init_db():
db = get_db()
db.execute(
"""
CREATE TABLE IF NOT EXISTS pages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
uuid TEXT NOT NULL UNIQUE,
title TEXT NOT NULL DEFAULT '',
html TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""
)
# Migrate: ensure title exists
try:
cols = {row[1] for row in db.execute("PRAGMA table_info(pages)").fetchall()}
if "title" not in cols:
db.execute("ALTER TABLE pages ADD COLUMN title TEXT NOT NULL DEFAULT ''")
except Exception:
pass
db.commit()
def init_app(app):
@app.before_request
def _before_request(): # noqa: D401
init_db()
@app.teardown_appcontext
def _teardown(_=None): # noqa: D401
close_db()

76
app/pages.py Normal file
View File

@@ -0,0 +1,76 @@
from flask import Blueprint, redirect, url_for, abort, Response
from .db import get_db
from .auth import is_logged_in
pages_bp = Blueprint("pages", __name__)
@pages_bp.route("/")
def home():
if not is_logged_in():
return redirect(url_for("auth.login"))
return redirect(url_for("admin.index"))
@pages_bp.route("/p/<string:uid>")
def view_page(uid: str):
db = get_db()
row = db.execute("SELECT html, title FROM pages WHERE uuid = ?", (uid,)).fetchone()
if row is None:
abort(404)
html: str = row["html"]
page_title = row["title"] or ""
if page_title:
lower = html.lower()
if "</head>" in lower:
i = lower.rfind("</head>")
html = html[:i] + f"<title>{page_title}</title>" + html[i:]
elif "<html" in lower:
if "<body" in lower:
j = lower.find("<body")
html = html[:j] + f"<head><meta charset=\"utf-8\"><title>{page_title}</title></head>" + html[j:]
else:
html = f"<head><meta charset=\"utf-8\"><title>{page_title}</title></head>" + html
# Admin button only for logged-in users
if is_logged_in():
admin_url = url_for("admin.index")
toolbar = (
'<div style="position:fixed;top:16px;right:16px;z-index:2147483647;">'
f'<a href="{admin_url}" '
'style="background:linear-gradient(135deg,#111,#333);color:#fff;padding:10px 14px;'
'border-radius:10px;box-shadow:0 8px 20px rgba(0,0,0,0.25);'
'text-decoration:none;font-weight:600;letter-spacing:.2px;'
'font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;"'
'>Админка</a></div>'
)
lower = html.lower()
if "</body>" in lower:
idx = lower.rfind("</body>")
html = html[:idx] + toolbar + html[idx:]
else:
html = html + toolbar
# Watermark for everyone (top-left)
wm = (
"<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">"
"<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>"
"<link href=\"https://fonts.googleapis.com/css2?family=Pacifico&display=swap\" rel=\"stylesheet\">"
"<div style=\"position:fixed;top:16px;left:16px;z-index:2147483647;"
"font-family:'Pacifico',cursive;letter-spacing:.2px;color:rgba(255,255,255,.92);"
"text-shadow:0 2px 8px rgba(0,0,0,.35);pointer-events:none;user-select:none;\">"
"Made by Ruslan"
"</div>"
)
lower_all = html.lower()
if "</body>" in lower_all:
i2 = lower_all.rfind("</body>")
html = html[:i2] + wm + html[i2:]
else:
html = html + wm
return Response(html, mimetype="text/html; charset=utf-8")