diff --git a/README.md b/README.md index 0a6f2a2..705142a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ data/ «Historikk»-seksjon i editoren med diff per endring; hele loggen ligger under Innstillinger (eller `git -C data/content log`). * **Gjester** ser og redigerer bare egne innlegg. Hovedbrukeren ser alt og administrerer gjester. +* **Signatur**: innlegg avsluttes med «– Visningsnavn», evt. med e-post som mailto-lenke. Begge + settes under Profil i admin og slås opp ved bygging, så en endring gjelder alle innlegg. ## Kjøre lokalt diff --git a/soapbox/admin.py b/soapbox/admin.py index 5fa156e..760cf67 100644 --- a/soapbox/admin.py +++ b/soapbox/admin.py @@ -61,6 +61,7 @@ bp = Blueprint( ) USERNAME_RE = re.compile(r"^[a-z0-9][a-z0-9-]{1,30}$") +EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$") # Reserverte navn som ville kollidert med URL-strukturen RESERVED = {"admin", "theme", "feed.xml", "posts", "guests", "index.html"} @@ -244,7 +245,7 @@ def new(): body="", date=_now(), draft=True, - author=g.user.display_name, + author=g.user.username, guest=guest, ) save_post(cfg, post) @@ -407,13 +408,16 @@ def users(): flash("Passordet må være minst 8 tegn.", "error") else: try: - db.create_user( + user = db.create_user( g.db, username, password, db.ROLE_GUEST, request.form.get("display_name", ""), ) + email = request.form.get("email", "").strip() + if email and EMAIL_RE.match(email): + db.update_profile(g.db, user.id, user.display_name, email) flash(f"Gjestebruker {username} opprettet.", "ok") except sqlite3.IntegrityError: flash("Brukernavnet er allerede i bruk.", "error") @@ -448,6 +452,22 @@ def password(): return render_template("admin/password.html") +@bp.route("/profile", methods=["GET", "POST"]) +@login_required +def profile(): + """Visningsnavn (signatur på innlegg) og e-post (vises offentlig hvis satt).""" + if request.method == "POST": + email = request.form.get("email", "").strip() + if email and not EMAIL_RE.match(email): + flash("Ugyldig e-postadresse.", "error") + else: + db.update_profile(g.db, g.user.id, request.form.get("display_name", ""), email) + build_site(_cfg()) # signaturen ligger i de genererte sidene + flash("Profilen er lagret.", "ok") + return redirect(url_for("admin.profile")) + return render_template("admin/profile.html", me=db.get_user(g.db, g.user.id)) + + @bp.route("/settings", methods=["GET", "POST"]) @owner_required def settings(): diff --git a/soapbox/build.py b/soapbox/build.py index 36b833c..be1d4cc 100644 --- a/soapbox/build.py +++ b/soapbox/build.py @@ -23,6 +23,7 @@ from xml.sax.saxutils import escape from jinja2 import ChoiceLoader, Environment, FileSystemLoader, select_autoescape +from . import db from .config import Config, load_site_settings from .content import IMAGE_EXT, Post, list_posts from .render import excerpt, relative_to, render_markdown @@ -50,6 +51,7 @@ def build_site(cfg: Config) -> int: site = load_site_settings(cfg) env = _env(cfg) posts = list_posts(cfg) + _resolve_authors(cfg, posts) out = cfg.public_dir # Bygg i en midlertidig katalog og bytt atomisk, så besøkende aldri ser @@ -90,7 +92,7 @@ def build_site(cfg: Config) -> int: for guest, gposts in guests.items(): _write( tmp / guest / "index.html", - render("index.html", f"{guest}/", posts=gposts, heading=gposts[0].author or guest), + render("index.html", f"{guest}/", posts=gposts, heading=gposts[0].author_name or guest), ) _write(tmp / "feed.xml", _feed(cfg, site, posts[:20])) @@ -113,6 +115,28 @@ def build_site(cfg: Config) -> int: return len(posts) +def _resolve_authors(cfg: Config, posts: list[Post]) -> None: + """Fyller author_name/author_email fra brukertabellen. + + Frontmatter lagrer brukernavnet (stabilt); visningsnavn og e-post kan + endres i profilen og slår da gjennom på alle innlegg ved neste bygg. + Finnes ikke brukeren (f.eks. innhold klonet uten brukerdatabase), vises + verdien fra frontmatter som den er. + """ + conn = db.connect(cfg.db_path) + try: + users = db.authors(conn) + finally: + conn.close() + for p in posts: + key = p.author or (p.guest or "") + user = users.get(key) + if user is None and p.guest is None: + user = next((u for u in users.values() if u.is_owner), None) + p.author_name = user.display_name if user else (p.author or p.guest or "") + p.author_email = user.email if user else "" + + def _sitemap(cfg: Config, posts: list[Post], guests: list[str]) -> str: """sitemap.xml med forsiden, alle innlegg og gjesteoversiktene.""" base = cfg.site_url + "/" @@ -145,7 +169,7 @@ def _feed(cfg: Config, site: dict, posts: list[Post]) -> str: f"{escape(link)}" f"{escape(link)}" f"{format_datetime(p.date)}" - + (f"{escape(p.author)}" if p.author else "") + + (f"{escape(p.author_name)}" if p.author_name else "") + f"{escape(excerpt(p.body))}" "" ) diff --git a/soapbox/content.py b/soapbox/content.py index bbbc0b8..c50742e 100644 --- a/soapbox/content.py +++ b/soapbox/content.py @@ -37,7 +37,9 @@ class Post: date: datetime updated: datetime | None = None draft: bool = False - author: str = "" # brukernavn; tomt for hovedbruker + author: str = "" # brukernavn (eldre innlegg kan ha visningsnavn her) + author_name: str = "" # visningsnavn, fylles ut ved bygging fra brukertabellen + author_email: str = "" guest: str | None = None # None for hovedbruker, ellers gjestens brukernavn path: Path | None = None images: list[str] = field(default_factory=list) diff --git a/soapbox/db.py b/soapbox/db.py index b890d3b..0424171 100644 --- a/soapbox/db.py +++ b/soapbox/db.py @@ -40,6 +40,7 @@ class User: username: str display_name: str role: str + email: str = "" @property def is_owner(self) -> bool: @@ -56,6 +57,11 @@ def connect(path: Path) -> sqlite3.Connection: conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") conn.executescript(SCHEMA) + # Enkel migrering: kolonner lagt til etter første versjon + cols = {r["name"] for r in conn.execute("PRAGMA table_info(users)")} + if "email" not in cols: + conn.execute("ALTER TABLE users ADD COLUMN email TEXT NOT NULL DEFAULT ''") + conn.commit() return conn @@ -87,7 +93,7 @@ def verify_password(password: str, stored: str) -> bool: def _row_to_user(row: sqlite3.Row | None) -> User | None: if row is None: return None - return User(row["id"], row["username"], row["display_name"], row["role"]) + return User(row["id"], row["username"], row["display_name"], row["role"], row["email"]) def create_user( @@ -124,6 +130,19 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> User return None +def update_profile(conn: sqlite3.Connection, user_id: int, display_name: str, email: str) -> None: + conn.execute( + "UPDATE users SET display_name=?, email=? WHERE id=?", + (display_name.strip(), email.strip(), user_id), + ) + conn.commit() + + +def authors(conn: sqlite3.Connection) -> dict[str, User]: + """Brukernavn → bruker, til oppslag av visningsnavn/e-post ved bygging.""" + return {u.username: u for u in list_users(conn)} + + def password_stamp(conn: sqlite3.Connection, user_id: int) -> str | None: """Kort, ikke-hemmelig fingeravtrykk av passord-hashen, lagret i sesjonen slik at et passordbytte ugyldiggjør alle eksisterende sesjoner.""" diff --git a/soapbox/templates/admin/base.html b/soapbox/templates/admin/base.html index e533f49..f74d98e 100644 --- a/soapbox/templates/admin/base.html +++ b/soapbox/templates/admin/base.html @@ -18,6 +18,7 @@ Brukere Innstillinger {% endif %} + Profil Passord Se bloggen
diff --git a/soapbox/templates/admin/profile.html b/soapbox/templates/admin/profile.html new file mode 100644 index 0000000..67956f6 --- /dev/null +++ b/soapbox/templates/admin/profile.html @@ -0,0 +1,14 @@ +{% extends "admin/base.html" %} +{% block title %}Profil{% endblock %} +{% block content %} +

Profil

+ + + + +

Brukernavn: {{ me.username }}{% if not me.is_owner %} (URL-prefiks /{{ me.username }}/){% endif %}

+ +
+{% endblock %} diff --git a/soapbox/templates/admin/users.html b/soapbox/templates/admin/users.html index ad07771..4b38443 100644 --- a/soapbox/templates/admin/users.html +++ b/soapbox/templates/admin/users.html @@ -5,7 +5,7 @@