239 lines
7.7 KiB
Markdown
239 lines
7.7 KiB
Markdown
mega—großer krümel geschafft 🍰🧩
|
||
Hier ist das **finale `README.md`** für den Stand „Login → Admin → Posts CRUD → i18n → Flash/Templating → DB/Qdrant Compose“. Es fasst die Reise (gestern→heute) zusammen, dokumentiert die wichtigen Entscheidungen, und enthält alle Befehle & Fixes, die wir unterwegs gebraucht haben.
|
||
|
||
---
|
||
|
||
# CrumbCRM – Minimal vNext (FastAPI + MariaDB + Qdrant)
|
||
|
||
Server-rendered, mehrsprachig, mit Login/Session, Admin-Bereich und einfachem Blog-CRUD.
|
||
Ziel: **schnell, stabil, kein Frontend-Build-Ballast**, dafür klare HTML-Templates (Jinja2), Forms, Flash-Messages und später andockbare Vektor-Suche (Qdrant).
|
||
|
||
## Was läuft aktuell?
|
||
|
||
* 🌐 **i18n** Pfade: `/de/…` und `/en/…` (Root `/` → 307 auf bevorzugte Sprache)
|
||
* 🔐 **Login** (Sessions + bcrypt) und Logout
|
||
* 👤 Rollen: `admin` vs `user` (403, wenn kein Admin)
|
||
* 🧰 **Admin Dashboard**: `/admin`
|
||
* ✍️ **Posts CRUD** für Admin:
|
||
|
||
* `GET /admin/posts` – Liste
|
||
* `GET /admin/posts/new` – Formular
|
||
* `POST /admin/posts/new` – Erstellen
|
||
* `GET /admin/posts/{id}/edit` – Bearbeiten
|
||
* `POST /admin/posts/{id}/edit` – Speichern
|
||
* ✨ **Flash-Nachrichten** (einmalige Anzeige nach Redirect)
|
||
* 🧪 **API Demo**: `GET /api/hello?lang=de|en`
|
||
* 🩺 **Health**: `GET /health`
|
||
* 🧠 **Qdrant** ist per Compose angebunden (noch ohne Ingest), UI unter `http://localhost:6333/dashboard`
|
||
|
||
## Stack
|
||
|
||
* **FastAPI**, **Jinja2**, **Starlette Sessions**
|
||
* **MariaDB** (PyMySQL)
|
||
* **passlib\[bcrypt]** (Password-Hashing)
|
||
* **python-multipart** (Form-POSTs)
|
||
* **Qdrant** (Vektoren; später Indexing/Embedding)
|
||
|
||
Empfohlene Pins (stehen in `app/requirements.txt`):
|
||
|
||
```
|
||
fastapi==0.115.0
|
||
uvicorn[standard]==0.30.6
|
||
jinja2==3.1.4
|
||
passlib[bcrypt]==1.7.4
|
||
bcrypt==4.1.3
|
||
python-multipart==0.0.9
|
||
PyMySQL==1.1.1
|
||
```
|
||
|
||
## Projektstruktur
|
||
|
||
```
|
||
app/
|
||
main.py # App, Routing, Session, Render-Helper (state.render)
|
||
requirements.txt
|
||
routers/
|
||
admin_post.py # Admin-CRUD für Posts
|
||
templates/
|
||
base.html
|
||
pages/
|
||
home.html
|
||
login.html
|
||
admin.html
|
||
posts/
|
||
index.html
|
||
new.html
|
||
edit.html
|
||
_edit_row.htm
|
||
compose/
|
||
docker-compose.yml
|
||
init/
|
||
01_schema.sql # users Tabelle
|
||
02_posts.sql # posts Tabelle
|
||
reset_admin_demo.sh # pass-hash Seeds (admin/demo)
|
||
data/
|
||
mysql/ # MariaDB Daten
|
||
qdrant/ # Qdrant Storage
|
||
```
|
||
|
||
## Start (Docker Compose)
|
||
|
||
```bash
|
||
cd compose
|
||
docker compose up --build
|
||
```
|
||
|
||
* App: [http://localhost:8000](http://localhost:8000)
|
||
* Qdrant UI: [http://localhost:6333/dashboard](http://localhost:6333/dashboard)
|
||
|
||
### Admin/Demo Benutzer anlegen (Seeds)
|
||
|
||
Wenn die Container laufen, einmalig die Benutzer mit bekannten Hashes setzen:
|
||
|
||
```bash
|
||
# Benutzer in DB einspielen (verwende -T, damit keine TTY-Probleme)
|
||
docker compose exec -T db sh -lc '
|
||
mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e "
|
||
INSERT INTO users (email, pass_hash, role, locale, display_name)
|
||
VALUES
|
||
(\"admin@crumb.local\", \"\$2b\$12\$H1V2q0iY8mqlz1xUbx7m5u4T0cVJH0hQk0q9o5aNq5Pjv1e0q9lby\", \"admin\", \"de\", \"Admin\"),
|
||
(\"demo@crumb.local\", \"\$2b\$12\$pI6cJ7gq4qkqF3gL2xjY4u9v9s1yN6wZQn9I7gG7xv7Cw0m3t8yVSe\", \"user\", \"de\", \"Demo\")
|
||
ON DUPLICATE KEY UPDATE pass_hash=VALUES(pass_hash), role=VALUES(role), locale=VALUES(locale);
|
||
"'
|
||
```
|
||
|
||
> **Hinweis:** `admin@crumb.local` hat Adminrechte. `demo@crumb.local` **nicht** – 403 auf `/admin` ist korrekt.
|
||
|
||
## Wichtige Routen (Stand heute)
|
||
|
||
* `GET /` → 307 auf `/de/`
|
||
* `GET /{lang}/` → Home
|
||
* `GET /{lang}/login` + `POST /{lang}/login` → Login
|
||
* `POST /logout` → Logout
|
||
* `GET /admin` → Admin-Start (nur Admin)
|
||
* `GET /admin/posts` → Post-Liste (nur Admin)
|
||
* `GET|POST /admin/posts/new` → Neu anlegen
|
||
* `GET|POST /admin/posts/{id}/edit` → Bearbeiten
|
||
* `GET /api/hello?lang=de|en` → einfache JSON-API
|
||
* `GET /health` → Healthcheck
|
||
|
||
*(Optional)* Öffentliche Post-Liste wäre `/de/posts` bzw. `/en/posts` – Route/Template ist noch nicht aktiv. Snippet unten.
|
||
|
||
## Flash-Nachrichten
|
||
|
||
* Wir speichern Flashs als Liste in `req.session["_flashes"]`.
|
||
* **Einmalige Anzeige**: `render()` poppt sie und setzt sie leer zurück.
|
||
* Wenn du direkt nach dem Redirect noch mal „hart“ navigierst, ist die Flash weg – das ist Absicht.
|
||
|
||
## Security Basics
|
||
|
||
* Session-Cookie: HttpOnly, `SameSite=Lax`
|
||
* CSRF: Forms sind serverseitig – bei Bedarf später Token ergänzen
|
||
* Rollenprüfung: `admin_required` schützt Admin-Routen
|
||
|
||
## Troubleshooting (die Fallen heute)
|
||
|
||
* **`python-multipart` fehlt** → in `requirements.txt` **mit** eintragen (ist drin).
|
||
* **`(trapped) error reading bcrypt version`**
|
||
→ sichere Kombi pinnen: `passlib[bcrypt]==1.7.4` **und** `bcrypt==4.1.3`.
|
||
* **`ImportError: circular import`**
|
||
→ Admin-Router importiert nur **Funktionen** aus einer kleinen `deps.py` (DB/ACL), nicht `main`.
|
||
* **`State has no attribute render`**
|
||
→ `state.render(req, tpl, **ctx)` wird **in `main.py`** beim Startup gesetzt. Router nutzt **genau** diese Funktion.
|
||
* **MariaDB Warnung „Aborted connection … Got an error reading communication packets“**
|
||
→ Fix: überall `with db() as conn:` verwenden (Kontextmanager schließt sauber).
|
||
* **`Field 'body_md' doesn't have a default value`**
|
||
→ Beim Insert/Edit `body_md` **immer** mitsenden (Form Feld ist vorhanden).
|
||
|
||
## Logs
|
||
|
||
```bash
|
||
docker compose logs -f app
|
||
docker compose logs -f db
|
||
```
|
||
|
||
## Optional: Öffentliche Posts-Liste
|
||
|
||
Wenn gewünscht, Route in `main.py` und Template ergänzen:
|
||
|
||
```python
|
||
# in main.py
|
||
from deps import db
|
||
from fastapi import Request
|
||
from fastapi.responses import HTMLResponse
|
||
|
||
@app.get("/{lang}/posts", response_class=HTMLResponse)
|
||
def public_posts(req: Request, lang: str):
|
||
with db() as conn:
|
||
with conn.cursor() as cur:
|
||
cur.execute("""
|
||
SELECT id, title, slug, locale, updated_at
|
||
FROM posts
|
||
WHERE is_published=1 AND locale=%s
|
||
ORDER BY IFNULL(updated_at, created_at) DESC, id DESC
|
||
""", (lang,))
|
||
rows = cur.fetchall()
|
||
return req.app.state.render(req, "pages/posts_public.html",
|
||
posts=rows, seo={"title": "Posts"})
|
||
```
|
||
|
||
```html
|
||
<!-- templates/pages/posts_public.html -->
|
||
{% extends 'base.html' %}
|
||
{% block content %}
|
||
<h1>Posts</h1>
|
||
<ul class="list">
|
||
{% for p in posts %}
|
||
<li><strong>{{ p.title }}</strong> <small>({{ p.locale }})</small>
|
||
{% if p.updated_at %} – <em>{{ p.updated_at }}</em>{% endif %}
|
||
</li>
|
||
{% else %}
|
||
<li>Keine veröffentlichten Beiträge.</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endblock %}
|
||
```
|
||
|
||
## DB-Schema (Minimal)
|
||
|
||
**users**
|
||
|
||
```
|
||
id, email (unique), pass_hash, role('admin'|'user'), locale, created_at
|
||
```
|
||
|
||
**posts**
|
||
|
||
```
|
||
id, title, slug, locale, is_published TINYINT, body_md MEDIUMTEXT,
|
||
created_at, updated_at
|
||
```
|
||
|
||
→ angelegt über `compose/init/01_schema.sql` und `02_posts.sql`.
|
||
|
||
## Befehls-Snippets (nützlich)
|
||
|
||
```bash
|
||
# DB-Ping
|
||
docker compose exec -T db sh -lc \
|
||
'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" -e "SELECT 1;"'
|
||
|
||
# User-Check
|
||
docker compose exec -T db sh -lc \
|
||
'mariadb -u"$MARIADB_USER" -p"$MARIADB_PASSWORD" "$MARIADB_DATABASE" \
|
||
-e "SELECT id,email,role,locale,created_at FROM users;"'
|
||
```
|
||
|
||
## Nächste Schritte (wenn du wieder Luft hast)
|
||
|
||
* Öffentliche Post-Ansicht `/de/posts` aktivieren
|
||
* CSRF-Token für Form-POSTs (kleiner Middleware-Helfer)
|
||
* Qdrant-Ingest-Worker (Markdown → Chunks → Embeddings → Upsert)
|
||
* Settings/ACL feiner (Rollenmatrix)
|
||
* Passwort-Reset/Mail (lokal via Mailpit/SMTP-Mock)
|
||
|
||
---
|
||
|
||
**Danke für den Ritt durch Pepper 🐍, Dumbo 🐘 & den Krümelwald 🌲.**
|
||
Von „nur Kuchen“ zu „Tortenbit“: Login-Loop steht, Admin schreibt, Flash funkt, i18n schaltet. Der Rest wird Feinschliff—aber die Basis trägt. Wuuuuhuuu! 🎉
|