Compare commits

...
2 Commits
Author SHA1 Message Date
nessi e7d288ff12 Add SMTP security mode selector with SSL/STARTTLS/none options and automatic port switching
Replaced boolean `smtp_use_tls` with explicit `smtp_security` enum field supporting "none", "starttls", and "ssl" modes. Added database migration to create `smtp_security` column with "starttls" default. Updated mailer to use SMTP_SSL client for direct SSL connections on port 465 and SMTP with STARTTLS for port 587. Modified admin settings UI to show dropdown selector with encryption options and auto-adjust
2026-08-02 10:38:50 +02:00
nessi 33caa8f792 Add entry label translation system for German-to-English game sheet items
Implemented `translateEntryLabel` helper function with mapping dictionary for translating German game entry labels (spells, potions, locations) to English equivalents. Added language parameter extraction in SheetSection component and applied translation to entry labels based on current language setting. Maintains German as source labels with English translations provided via lookup table.
2026-08-02 10:36:44 +02:00
7 changed files with 52 additions and 8 deletions
+6 -2
View File
@@ -23,9 +23,13 @@ def send_html_email(settings: AppSettings, recipient: str, subject: str, body_ht
message.add_alternative(body_html, subtype="html")
context = ssl.create_default_context()
with smtplib.SMTP(settings.smtp_host, settings.smtp_port or 587, timeout=20) as server:
security = getattr(settings, "smtp_security", "") or ("starttls" if settings.smtp_use_tls else "none")
port = settings.smtp_port or (465 if security == "ssl" else 587)
smtp_client = smtplib.SMTP_SSL if security == "ssl" else smtplib.SMTP
connection = smtp_client(settings.smtp_host, port, timeout=20, context=context) if security == "ssl" else smtp_client(settings.smtp_host, port, timeout=20)
with connection as server:
server.ehlo()
if settings.smtp_use_tls:
if security == "starttls":
server.starttls(context=context)
server.ehlo()
if settings.smtp_username:
+8
View File
@@ -88,6 +88,14 @@ Very small, pragmatic auto-migration (no alembic).
- supports old schema (join_code/chip_code) and new schema (code/chip)
"""
# --- app settings: explicit SMTP security mode (none/starttls/ssl) ---
if not _has_column(db, "app_settings", "smtp_security"):
try:
db.execute(text("ALTER TABLE app_settings ADD COLUMN smtp_security VARCHAR DEFAULT 'starttls'"))
db.commit()
except Exception:
db.rollback()
# --- users.display_name ---
if not _has_column(db, "users", "display_name"):
try:
+1
View File
@@ -52,6 +52,7 @@ class AppSettings(Base):
smtp_from_email: Mapped[str] = mapped_column(String, default="")
smtp_from_name: Mapped[str] = mapped_column(String, default="Cluedo HP")
smtp_use_tls: Mapped[bool] = mapped_column(Boolean, default=True)
smtp_security: Mapped[str] = mapped_column(String, default="starttls")
app_base_url: Mapped[str] = mapped_column(String, default="http://localhost:8081")
+6 -1
View File
@@ -163,6 +163,7 @@ def read_smtp_settings(req: Request, db: Session = Depends(get_db)):
"smtp_password_configured": bool(settings.smtp_password),
"smtp_from_email": settings.smtp_from_email,
"smtp_from_name": settings.smtp_from_name,
"smtp_security": getattr(settings, "smtp_security", "starttls") or ("starttls" if settings.smtp_use_tls else "none"),
"smtp_use_tls": settings.smtp_use_tls,
"app_base_url": settings.app_base_url,
}
@@ -181,7 +182,11 @@ def update_smtp_settings(req: Request, data: dict, db: Session = Depends(get_db)
settings.smtp_username = (data.get("smtp_username") or "").strip()
settings.smtp_from_email = (data.get("smtp_from_email") or "").strip()
settings.smtp_from_name = (data.get("smtp_from_name") or "Cluedo HP").strip()
settings.smtp_use_tls = bool(data.get("smtp_use_tls", True))
security = data.get("smtp_security") or ("starttls" if data.get("smtp_use_tls", True) else "none")
if security not in ("none", "starttls", "ssl"):
raise HTTPException(400, "invalid SMTP security mode")
settings.smtp_security = security
settings.smtp_use_tls = security == "starttls"
settings.app_base_url = (data.get("app_base_url") or "http://localhost:8081").strip().rstrip("/")
if "smtp_password" in data and data.get("smtp_password"):
settings.smtp_password = data["smtp_password"]
@@ -7,7 +7,7 @@ import { useLanguage } from "../i18n";
const initial = {
smtp_host: "", smtp_port: 587, smtp_username: "", smtp_password: "",
smtp_from_email: "", smtp_from_name: "Cluedo HP", smtp_use_tls: true,
smtp_from_email: "", smtp_from_name: "Cluedo HP", smtp_security: "starttls",
app_base_url: window.location.origin,
};
@@ -72,7 +72,11 @@ export default function AdminSettingsModal({ open, onClose }) {
<label style={styles.adminFieldLabel}>{t("senderName")}<input value={form.smtp_from_name} onChange={(e) => setField("smtp_from_name", e.target.value)} style={styles.input} /></label>
</div>
<label style={styles.adminFieldLabel}>{t("appUrl")}<input value={form.app_base_url} onChange={(e) => setField("app_base_url", e.target.value)} placeholder="https://notizbogen.example.com" style={styles.input} /></label>
<label className="hp-admin-check"><input type="checkbox" checked={!!form.smtp_use_tls} onChange={(e) => setField("smtp_use_tls", e.target.checked)} /> {t("useTls")}</label>
<label style={styles.adminFieldLabel}>{language === "en" ? "SMTP encryption" : "SMTP-Verschlüsselung"}<select value={form.smtp_security || "starttls"} onChange={(e) => { const security = e.target.value; setField("smtp_security", security); if (security === "ssl" && String(form.smtp_port) === "587") setField("smtp_port", 465); if (security === "starttls" && String(form.smtp_port) === "465") setField("smtp_port", 587); }} style={styles.input}>
<option value="none">{language === "en" ? "None" : "Keine Verschlüsselung"}</option>
<option value="starttls">STARTTLS</option>
<option value="ssl">{language === "en" ? "TLS/SSL (direct, port 465)" : "TLS/SSL (direkt, Port 465)"}</option>
</select></label>
<div className="hp-settings-test">
<label style={{ ...styles.adminFieldLabel, flex: 1 }}>{t("testEmail")}<input type="email" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="you@example.com" style={styles.input} /></label>
<button onClick={test} style={styles.secondaryBtn} disabled={testing}>{testing ? (language === "en" ? "Sending …" : "Sende …") : t("sendTest")}</button>
+3 -3
View File
@@ -2,7 +2,7 @@
import React from "react";
import { styles } from "../styles/styles";
import { stylesTokens } from "../styles/theme";
import { useLanguage } from "../i18n";
import { translateEntryLabel, useLanguage } from "../i18n";
export default function SheetSection({
title,
@@ -13,7 +13,7 @@ export default function SheetSection({
displayTag,
readOnly = false,
}) {
const { t } = useLanguage();
const { language, t } = useLanguage();
const getRowBg = (status) => {
if (status === 1) return stylesTokens.rowNoBg;
if (status === 2) return stylesTokens.rowOkBg;
@@ -83,7 +83,7 @@ export default function SheetSection({
}}
title={readOnly ? t("readOnly") : t("cycleStatus")}
>
{e.label}
{translateEntryLabel(e.label, language)}
</div>
<div style={styles.statusCell}>
+22
View File
@@ -3,6 +3,28 @@ import React, { createContext, useContext, useEffect, useMemo, useState } from "
const STORAGE_KEY = "hpLanguage";
const LanguageContext = createContext(null);
const entryLabels = {
"Schlaftrunk": "Sleeping Potion",
"Verschwindekabinett": "Vanishing Cabinet",
"Portschlüssel": "Portkey",
"Impedimenta": "Impedimenta",
"Petrificus Totalus": "Petrificus Totalus",
"Alraune": "Mandrake",
"Große Halle": "Great Hall",
"Krankenflügel": "Hospital Wing",
"Raum der Wünsche": "Room of Requirement",
"Klassenzimmer für Zaubertränke": "Potions Classroom",
"Pokalszimmer": "Trophy Room",
"Klassenzimmer für Wahrsagen": "Divination Classroom",
"Eulerei": "Owlery",
"Bibliothek": "Library",
"Verteidigung gegen die dunklen Künste": "Defence Against the Dark Arts",
};
export function translateEntryLabel(label, language) {
return language === "en" ? (entryLabels[label] || label) : label;
}
export const translations = {
de: {
language: "Deutsch",