From 85eaf6a99af0b3f8376f071f1557d7b1c4b3d262 Mon Sep 17 00:00:00 2001
From: RGalyaviev
Date: Wed, 3 Sep 2025 14:37:36 +0300
Subject: [PATCH] UI polish: global modern styles; Admin button pinned top-left
across app and generated pages; index redirects to login for guests
---
.gitignore | 63 +++++++++++++++
README.md | 51 ++++++++++++
app.py | 182 +++++++++++++++++++++++++++++++++++++++++++
requirements.txt | 1 +
templates/admin.html | 40 ++++++++++
templates/base.html | 115 +++++++++++++++++++++++++++
templates/login.html | 16 ++++
7 files changed, 468 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 app.py
create mode 100644 requirements.txt
create mode 100644 templates/admin.html
create mode 100644 templates/base.html
create mode 100644 templates/login.html
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..549df70
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,63 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# Virtual environments
+.venv/
+venv/
+ENV/
+env/
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# SQLite database
+*.db
+
+# IDEs and editors
+.vscode/
+.idea/
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# OS
+.DS_Store
+Thumbs.db
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..cf38998
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# Flask UUID Pages Admin (SQLite)
+
+Минималистичное Flask‑приложение с SQLite:
+- Админка доступна только пользователю `ruslan` с паролем `utOgbZ09ruslan` (по умолчанию).
+- В админке можно вставлять HTML и публиковать страницы.
+- Для каждой страницы генерируется UUID‑ссылка `/p/`.
+- Неавторизованный пользователь не видит админку и главную страницу (404). Страницы по UUID доступны всем, у кого есть ссылка.
+
+## Запуск локально
+
+1. Создайте и активируйте виртуальное окружение (опционально):
+
+ Windows PowerShell:
+
+ ```powershell
+ python -m venv .venv
+ .venv\\Scripts\\Activate.ps1
+ ```
+
+2. Установите зависимости:
+
+ ```powershell
+ pip install -r requirements.txt
+ ```
+
+3. (Опционально) Переопределите настройки через переменные окружения:
+
+ ```powershell
+ $env:SECRET_KEY = "your-strong-secret-key"
+ $env:ADMIN_USERNAME = "ruslan"
+ $env:ADMIN_PASSWORD = "utOgbZ09ruslan"
+ ```
+
+4. Запустите сервер разработки:
+
+ ```powershell
+ python app.py
+ ```
+
+## Маршруты
+
+- `/login` — форма входа в админку.
+- `/admin` — админ‑панель (только для авторизованных, иначе 404).
+- `/p/` — просмотр опубликованной страницы по UUID.
+- `/` — редирект в админку для авторизованных, иначе 404.
+
+## Замечания по безопасности
+
+- HTML хранится и отдаётся как есть. Предполагается, что контент вводит доверенный админ.
+- В продакшне обязательно задайте `SECRET_KEY` и используйте HTTPS; настройте защищённые cookies.
+
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..3791359
--- /dev/null
+++ b/app.py
@@ -0,0 +1,182 @@
+import os
+import sqlite3
+import uuid as uuid_lib
+from datetime import datetime
+
+from flask import (
+ Flask,
+ g,
+ render_template,
+ request,
+ redirect,
+ url_for,
+ session,
+ abort,
+ flash,
+ Response,
+)
+
+
+def create_app():
+ app = Flask(__name__)
+
+ # Basic config
+ app.config["SECRET_KEY"] = os.environ.get(
+ "SECRET_KEY",
+ # For production, override via env var. This default is for local/dev only.
+ "dev-secret-change-me",
+ )
+ app.config["DATABASE"] = os.path.join(os.path.dirname(__file__), "app.db")
+
+ # Admin credentials (can be overridden via env)
+ app.config["ADMIN_USERNAME"] = os.environ.get("ADMIN_USERNAME", "ruslan")
+ app.config["ADMIN_PASSWORD"] = os.environ.get("ADMIN_PASSWORD", "utOgbZ09ruslan")
+
+ # --------------------
+ # Database helpers
+ # --------------------
+ def get_db():
+ if "db" not in g:
+ g.db = sqlite3.connect(app.config["DATABASE"]) # type: ignore[attr-defined]
+ g.db.row_factory = sqlite3.Row
+ return g.db
+
+ def close_db(e=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,
+ html TEXT NOT NULL,
+ created_at TEXT NOT NULL
+ );
+ """
+ )
+ db.commit()
+
+ @app.before_request
+ def _before_request():
+ # Ensure DB exists
+ init_db()
+
+ @app.teardown_appcontext
+ def _teardown_appcontext(_=None):
+ close_db()
+
+ # --------------------
+ # Auth helpers
+ # --------------------
+ def is_logged_in() -> bool:
+ return bool(session.get("logged_in"))
+
+ def login_required():
+ if not is_logged_in():
+ # For non-auth users: show nothing (404), not even an admin hint
+ abort(404)
+
+ @app.context_processor
+ def inject_auth():
+ return {"logged_in": is_logged_in()}
+
+ # --------------------
+ # Routes
+ # --------------------
+ @app.route("/")
+ def index():
+ # Redirect unauthenticated users to login; authenticated users to admin.
+ if not is_logged_in():
+ return redirect(url_for("login"))
+ return redirect(url_for("admin"))
+
+ @app.route("/login", methods=["GET", "POST"])
+ def login():
+ if request.method == "POST":
+ username = request.form.get("username", "")
+ password = request.form.get("password", "")
+ if (
+ username == app.config["ADMIN_USERNAME"]
+ and password == app.config["ADMIN_PASSWORD"]
+ ):
+ session["logged_in"] = True
+ flash("Успешный вход в админку.", "success")
+ return redirect(url_for("admin"))
+ flash("Неверные имя пользователя или пароль.", "error")
+ return render_template("login.html")
+
+ @app.route("/logout", methods=["POST"])
+ def logout():
+ session.clear()
+ # After logout, nothing is visible. Return 404-like behavior.
+ abort(404)
+
+ @app.route("/admin", methods=["GET", "POST"])
+ def admin():
+ login_required()
+ db = get_db()
+ if request.method == "POST":
+ html = request.form.get("html", "").strip()
+ if not html:
+ flash("HTML не может быть пустым.", "error")
+ else:
+ uid = uuid_lib.uuid4().hex
+ db.execute(
+ "INSERT INTO pages (uuid, html, created_at) VALUES (?, ?, ?)",
+ (uid, html, datetime.utcnow().isoformat(timespec="seconds")),
+ )
+ db.commit()
+ flash("Страница опубликована.", "success")
+ return redirect(url_for("admin"))
+
+ pages = db.execute(
+ "SELECT id, uuid, created_at FROM pages ORDER BY id DESC"
+ ).fetchall()
+ base_url = request.host_url.rstrip("/")
+ return render_template("admin.html", pages=pages, base_url=base_url)
+
+ @app.route("/p/")
+ def view_page(uid: str):
+ db = get_db()
+ row = db.execute("SELECT html FROM pages WHERE uuid = ?", (uid,)).fetchone()
+ if row is None:
+ abort(404)
+ # Show a floating admin button for authenticated users, otherwise serve raw HTML
+ html: str = row["html"]
+ if is_logged_in():
+ admin_url = url_for("admin")
+ toolbar = (
+ ''
+ )
+ lower = html.lower()
+ if "
+
+
+ {{ title or 'Админка' }}
+
+
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %}
+ {% if messages %}
+ {% for category, message in messages %}
+
{{ message }}
+ {% endfor %}
+ {% endif %}
+ {% endwith %}
+
+ {% block content %}{% endblock %}
+
+
+ " in lower:
+ idx = lower.rfind("")
+ html = html[:idx] + toolbar + html[idx:]
+ else:
+ html = html + toolbar
+ return Response(html, mimetype="text/html; charset=utf-8")
+
+ # Optional: simple 404 page to keep things minimal
+ @app.errorhandler(404)
+ def not_found(_):
+ return ("", 404)
+
+ return app
+
+
+app = create_app()
+
+
+if __name__ == "__main__":
+ # Run development server
+ app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=True)
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..01a5fbb
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+Flask>=2.3,<3.0
diff --git a/templates/admin.html b/templates/admin.html
new file mode 100644
index 0000000..cce2185
--- /dev/null
+++ b/templates/admin.html
@@ -0,0 +1,40 @@
+{% extends 'base.html' %}
+{% block content %}
+
Страниц пока нет.
+ {% endif %}
+{% endblock %}
+
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..670b21b
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,115 @@
+
+
+