commit 85eaf6a99af0b3f8376f071f1557d7b1c4b3d262 Author: RGalyaviev Date: Wed Sep 3 14:37:36 2025 +0300 UI polish: global modern styles; Admin button pinned top-left across app and generated pages; index redirects to login for guests 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 = ( + '
' + f'Админка
' + ) + lower = html.lower() + if "" 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 %} +

Публикация HTML-страниц

+ +
+
+ + +
+ + После публикации создаётся ссылка с UUID. +
+ +

Опубликованные страницы

+ {% if pages %} + + + + + + + + + + + {% for p in pages %} + + + + + + + {% endfor %} + +
IDUUIDСсылкаСоздано
{{ p.id }}{{ p.uuid }}{{ base_url }}/p/{{ p.uuid }}{{ p.created_at }}
+ {% else %} +

Страниц пока нет.

+ {% 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 @@ + + + + + + {{ title or 'Админка' }} + + + +
+
+
{{ title or 'Админка' }}
+ +
+ +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..f67084a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} +{% block content %} +

Вход в админку

+
+
+ + +
+
+ + +
+ +
+{% endblock %} +