update
This commit is contained in:
@@ -34,7 +34,7 @@ from aza_style import (
|
|||||||
|
|
||||||
_MODULE_DESCRIPTIONS = {
|
_MODULE_DESCRIPTIONS = {
|
||||||
"ki": "Medizinische Fragen stellen,\nBefunde besprechen, Zweitmeinung einholen",
|
"ki": "Medizinische Fragen stellen,\nBefunde besprechen, Zweitmeinung einholen",
|
||||||
"kg": "Diktat aufnehmen, transkribieren\nund Krankengeschichte erstellen",
|
"kg": "Diktat aufnehmen und in Krankengeschichte umwandeln",
|
||||||
"empfang": "Empfangs-Chat, Aufgaben\nund Praxis-Kommunikation",
|
"empfang": "Empfangs-Chat, Aufgaben\nund Praxis-Kommunikation",
|
||||||
"notizen": "Sprachaufnahmen und Notizen\nfuer den Praxisalltag",
|
"notizen": "Sprachaufnahmen und Notizen\nfuer den Praxisalltag",
|
||||||
"translator": "Medizinische Fachtexte uebersetzen\nund Begriffe nachschlagen",
|
"translator": "Medizinische Fachtexte uebersetzen\nund Begriffe nachschlagen",
|
||||||
@@ -69,6 +69,7 @@ def _draw_module_icon(c: tk.Canvas, key: str):
|
|||||||
fill=fg, outline="")
|
fill=fg, outline="")
|
||||||
|
|
||||||
elif key == "kg":
|
elif key == "kg":
|
||||||
|
# Nur Fallback wenn logo.png fehlt (Kachel nutzt sonst echtes Logo als PhotoImage)
|
||||||
c.create_text(m, m, text="AzA", font=("Segoe UI", 11, "bold"), fill=fg)
|
c.create_text(m, m, text="AzA", font=("Segoe UI", 11, "bold"), fill=fg)
|
||||||
|
|
||||||
elif key == "notizen":
|
elif key == "notizen":
|
||||||
@@ -174,6 +175,7 @@ class AzaLauncher(tk.Tk):
|
|||||||
self.attributes("-topmost", True)
|
self.attributes("-topmost", True)
|
||||||
|
|
||||||
self._logo_img = None
|
self._logo_img = None
|
||||||
|
self._kg_tile_icon = None # gleiches logo.png wie Header, 38×38 für AzA-Office-Kachel
|
||||||
try:
|
try:
|
||||||
import sys as _sys
|
import sys as _sys
|
||||||
_search = []
|
_search = []
|
||||||
@@ -190,8 +192,17 @@ class AzaLauncher(tk.Tk):
|
|||||||
break
|
break
|
||||||
if logo_path:
|
if logo_path:
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
img = Image.open(logo_path).resize((44, 44), Image.Resampling.LANCZOS)
|
_pil = Image.open(logo_path)
|
||||||
self._logo_img = ImageTk.PhotoImage(img, master=self)
|
if _pil.mode not in ("RGB", "RGBA"):
|
||||||
|
_pil = _pil.convert("RGBA")
|
||||||
|
self._logo_img = ImageTk.PhotoImage(
|
||||||
|
_pil.resize((82, 82), Image.Resampling.LANCZOS),
|
||||||
|
master=self,
|
||||||
|
)
|
||||||
|
self._kg_tile_icon = ImageTk.PhotoImage(
|
||||||
|
_pil.resize((_ICON_SZ, _ICON_SZ), Image.Resampling.LANCZOS),
|
||||||
|
master=self,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -248,12 +259,12 @@ class AzaLauncher(tk.Tk):
|
|||||||
title_block = tk.Frame(title_row, bg=BG)
|
title_block = tk.Frame(title_row, bg=BG)
|
||||||
title_block.pack(side="left", anchor="w")
|
title_block.pack(side="left", anchor="w")
|
||||||
aza_lbl = tk.Label(title_block, text="AzA",
|
aza_lbl = tk.Label(title_block, text="AzA",
|
||||||
font=(FONT_FAMILY, 24, "bold"), fg=ACCENT, bg=BG,
|
font=(FONT_FAMILY, 19, "bold"), fg="#1a4d6d", bg=BG,
|
||||||
cursor="hand2")
|
cursor="hand2")
|
||||||
aza_lbl.pack(anchor="w")
|
aza_lbl.pack(anchor="w")
|
||||||
aza_lbl.bind("<Double-Button-1>", self._open_admin)
|
aza_lbl.bind("<Double-Button-1>", self._open_admin)
|
||||||
tk.Label(title_block, text="Medizinischer KI-Arbeitsplatz",
|
tk.Label(title_block, text="von Arzt zu Arzt",
|
||||||
font=(FONT_FAMILY, 10), fg=SUBTLE, bg=BG
|
font=(FONT_FAMILY, 11), fg="#1a4d6d", bg=BG
|
||||||
).pack(anchor="w")
|
).pack(anchor="w")
|
||||||
|
|
||||||
self._build_capacity_bar(header)
|
self._build_capacity_bar(header)
|
||||||
@@ -528,14 +539,15 @@ class AzaLauncher(tk.Tk):
|
|||||||
top_row = tk.Frame(inner, bg=_cbg, cursor=_cursor)
|
top_row = tk.Frame(inner, bg=_cbg, cursor=_cursor)
|
||||||
top_row.pack(fill="x", pady=(0, 10))
|
top_row.pack(fill="x", pady=(0, 10))
|
||||||
|
|
||||||
if mod_key == "kg" and self._logo_img:
|
icon_cv = tk.Canvas(top_row, width=_ICON_SZ, height=_ICON_SZ,
|
||||||
icon_lbl = tk.Label(top_row, image=self._logo_img, bg=_cbg, cursor=_cursor)
|
bg=icon_color, highlightthickness=0, cursor=_cursor)
|
||||||
icon_lbl.image = self._logo_img
|
icon_cv.pack(side="left")
|
||||||
icon_lbl.pack(side="left")
|
if mod_key == "kg" and getattr(self, "_kg_tile_icon", None) is not None:
|
||||||
|
icon_cv.create_image(
|
||||||
|
_ICON_SZ // 2, _ICON_SZ // 2,
|
||||||
|
image=self._kg_tile_icon,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
icon_cv = tk.Canvas(top_row, width=_ICON_SZ, height=_ICON_SZ,
|
|
||||||
bg=icon_color, highlightthickness=0, cursor=_cursor)
|
|
||||||
icon_cv.pack(side="left")
|
|
||||||
_draw_module_icon(icon_cv, mod_key)
|
_draw_module_icon(icon_cv, mod_key)
|
||||||
|
|
||||||
lbl_title = tk.Label(inner, text=label, font=(FONT_FAMILY, 12, "bold"),
|
lbl_title = tk.Label(inner, text=label, font=(FONT_FAMILY, 12, "bold"),
|
||||||
|
|||||||
@@ -1601,6 +1601,19 @@ def telemetry_ping(data: TelemetryPing, request: Request):
|
|||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_empfang_practice_record(practice_id: str, admin_email: str, display_name: str) -> None:
|
||||||
|
"""Legt die Praxis in empfang_practices.json an (idempotent)."""
|
||||||
|
try:
|
||||||
|
from empfang_routes import _ensure_practice
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[LICENSE] empfang_routes import failed: {exc}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
_ensure_practice(practice_id, name=display_name, admin_email=admin_email or "")
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[LICENSE] _ensure_practice failed: {exc}")
|
||||||
|
|
||||||
|
|
||||||
@app.get("/admin/telemetry/stats")
|
@app.get("/admin/telemetry/stats")
|
||||||
def telemetry_stats():
|
def telemetry_stats():
|
||||||
uptime_seconds = int((datetime.utcnow() - _server_start_time).total_seconds())
|
uptime_seconds = int((datetime.utcnow() - _server_start_time).total_seconds())
|
||||||
@@ -1664,6 +1677,7 @@ def license_status(
|
|||||||
status = None
|
status = None
|
||||||
current_period_end = None
|
current_period_end = None
|
||||||
customer_email = None
|
customer_email = None
|
||||||
|
license_practice_id: Optional[str] = None
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
import stripe_routes # type: ignore
|
import stripe_routes # type: ignore
|
||||||
@@ -1677,7 +1691,7 @@ def license_status(
|
|||||||
if license_key and license_key.strip():
|
if license_key and license_key.strip():
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
"""
|
||||||
SELECT status, current_period_end, customer_email
|
SELECT status, current_period_end, customer_email, practice_id
|
||||||
FROM licenses
|
FROM licenses
|
||||||
WHERE upper(license_key) = ?
|
WHERE upper(license_key) = ?
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
@@ -1688,7 +1702,7 @@ def license_status(
|
|||||||
if row is None and email and email.strip():
|
if row is None and email and email.strip():
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
"""
|
||||||
SELECT status, current_period_end, customer_email
|
SELECT status, current_period_end, customer_email, practice_id
|
||||||
FROM licenses
|
FROM licenses
|
||||||
WHERE lower(customer_email) = ?
|
WHERE lower(customer_email) = ?
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
@@ -1697,29 +1711,17 @@ def license_status(
|
|||||||
(email.strip().lower(),),
|
(email.strip().lower(),),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row is None:
|
if row is None:
|
||||||
n_active = con.execute(
|
print("[LICENSE-STATUS] keine eindeutige Lizenzzeile (license_key oder customer_email erforderlich)")
|
||||||
"SELECT COUNT(*) FROM licenses WHERE status = 'active'"
|
|
||||||
).fetchone()[0]
|
|
||||||
if n_active == 1:
|
|
||||||
row = con.execute(
|
|
||||||
"""
|
|
||||||
SELECT status, current_period_end, customer_email
|
|
||||||
FROM licenses
|
|
||||||
WHERE status = 'active'
|
|
||||||
LIMIT 1
|
|
||||||
"""
|
|
||||||
).fetchone()
|
|
||||||
print(f"[LICENSE-STATUS] fallback: single active license -> {row[2] if row else 'none'}")
|
|
||||||
else:
|
|
||||||
print(f"[LICENSE-STATUS] fallback: {n_active} active licenses, no auto-select")
|
|
||||||
if row:
|
if row:
|
||||||
status = row[0]
|
status = row[0]
|
||||||
current_period_end = int(row[1]) if row[1] is not None else None
|
current_period_end = int(row[1]) if row[1] is not None else None
|
||||||
customer_email = str(row[2]).strip() if row[2] is not None else None
|
customer_email = str(row[2]).strip() if row[2] is not None else None
|
||||||
|
license_practice_id = str(row[3]).strip() if len(row) > 3 and row[3] else None
|
||||||
except Exception:
|
except Exception:
|
||||||
status = None
|
status = None
|
||||||
current_period_end = None
|
current_period_end = None
|
||||||
customer_email = None
|
customer_email = None
|
||||||
|
license_practice_id = None
|
||||||
|
|
||||||
decision = compute_license_decision(current_period_end=current_period_end, status=status)
|
decision = compute_license_decision(current_period_end=current_period_end, status=status)
|
||||||
|
|
||||||
@@ -1736,11 +1738,14 @@ def license_status(
|
|||||||
"used_devices": 0,
|
"used_devices": 0,
|
||||||
"device_allowed": True,
|
"device_allowed": True,
|
||||||
"reason": "ok",
|
"reason": "ok",
|
||||||
|
"practice_id": license_practice_id or None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dev_user_key = license_practice_id.strip() if license_practice_id and license_practice_id.strip() else "default"
|
||||||
|
|
||||||
if device_id and customer_email:
|
if device_id and customer_email:
|
||||||
dd = enforce_and_touch_device(
|
dd = enforce_and_touch_device(
|
||||||
customer_email=customer_email, user_key="default",
|
customer_email=customer_email, user_key=dev_user_key,
|
||||||
device_id=device_id, db_path=str(db_path),
|
device_id=device_id, db_path=str(db_path),
|
||||||
device_name=device_name, app_version=app_version,
|
device_name=device_name, app_version=app_version,
|
||||||
device_fingerprint=device_fingerprint,
|
device_fingerprint=device_fingerprint,
|
||||||
@@ -1784,7 +1789,7 @@ def license_activate(
|
|||||||
row = con.execute(
|
row = con.execute(
|
||||||
"""
|
"""
|
||||||
SELECT subscription_id, status, current_period_end, customer_email,
|
SELECT subscription_id, status, current_period_end, customer_email,
|
||||||
allowed_users, devices_per_user
|
allowed_users, devices_per_user, practice_id
|
||||||
FROM licenses
|
FROM licenses
|
||||||
WHERE upper(license_key) = ?
|
WHERE upper(license_key) = ?
|
||||||
ORDER BY updated_at DESC
|
ORDER BY updated_at DESC
|
||||||
@@ -1796,9 +1801,30 @@ def license_activate(
|
|||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Lizenzschluessel ungueltig.")
|
raise HTTPException(status_code=404, detail="Lizenzschluessel ungueltig.")
|
||||||
|
|
||||||
sub_id, status, cpe, cust_email, au, dpu = row
|
sub_id, status, cpe, cust_email, au, dpu = row[:6]
|
||||||
|
practice_id_raw = row[6] if len(row) > 6 else None
|
||||||
current_period_end = int(cpe) if cpe is not None else None
|
current_period_end = int(cpe) if cpe is not None else None
|
||||||
|
|
||||||
|
practice_id = (str(practice_id_raw).strip() if practice_id_raw else "") or ""
|
||||||
|
if not practice_id:
|
||||||
|
practice_id = f"prac_{uuid.uuid4().hex[:12]}"
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(db_path) as con:
|
||||||
|
con.execute(
|
||||||
|
"""
|
||||||
|
UPDATE licenses
|
||||||
|
SET practice_id = ?, updated_at = ?
|
||||||
|
WHERE subscription_id = ?
|
||||||
|
""",
|
||||||
|
(practice_id, int(time.time()), sub_id),
|
||||||
|
)
|
||||||
|
con.commit()
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[LICENSE] practice_id update failed: {exc}")
|
||||||
|
|
||||||
|
display_name = (str(cust_email).strip() if cust_email else "") or "Meine Praxis"
|
||||||
|
_sync_empfang_practice_record(practice_id, str(cust_email or "").strip(), display_name)
|
||||||
|
|
||||||
decision = compute_license_decision(current_period_end=current_period_end, status=status)
|
decision = compute_license_decision(current_period_end=current_period_end, status=status)
|
||||||
|
|
||||||
device_id = request.headers.get("X-Device-Id")
|
device_id = request.headers.get("X-Device-Id")
|
||||||
@@ -1823,11 +1849,14 @@ def license_activate(
|
|||||||
"used_devices": 0,
|
"used_devices": 0,
|
||||||
"device_allowed": True,
|
"device_allowed": True,
|
||||||
"reason": "ok",
|
"reason": "ok",
|
||||||
|
"practice_id": practice_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dev_user_key = practice_id.strip() if practice_id.strip() else "default"
|
||||||
|
|
||||||
if cust_email:
|
if cust_email:
|
||||||
dd = enforce_and_touch_device(
|
dd = enforce_and_touch_device(
|
||||||
customer_email=cust_email, user_key="default",
|
customer_email=cust_email, user_key=dev_user_key,
|
||||||
device_id=device_id, db_path=str(db_path),
|
device_id=device_id, db_path=str(db_path),
|
||||||
device_name=device_name, app_version=app_version,
|
device_name=device_name, app_version=app_version,
|
||||||
device_fingerprint=device_fingerprint,
|
device_fingerprint=device_fingerprint,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
7
AzA march 2026/empfang_app_settings.json
Normal file
7
AzA march 2026/empfang_app_settings.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"x": 1181,
|
||||||
|
"y": 224,
|
||||||
|
"width": 480,
|
||||||
|
"height": 820,
|
||||||
|
"on_top": false
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -48,10 +48,20 @@
|
|||||||
"dermatology"
|
"dermatology"
|
||||||
],
|
],
|
||||||
"ui_font_delta": 0,
|
"ui_font_delta": 0,
|
||||||
"global_right_click_paste": true,
|
"global_right_click_paste": false,
|
||||||
"todo_auto_open": false,
|
"todo_auto_open": false,
|
||||||
"autocopy_after_diktat": true,
|
"autocopy_after_diktat": true,
|
||||||
"kommentare_auto_open": true,
|
"kommentare_auto_open": true,
|
||||||
|
"empfang_auto_open": false,
|
||||||
|
"empfang_was_open": false,
|
||||||
|
"empfang_prefs": {
|
||||||
|
"show_patient": false,
|
||||||
|
"show_ther": false,
|
||||||
|
"show_proc": false,
|
||||||
|
"show_kom": false,
|
||||||
|
"last_patient": "",
|
||||||
|
"geometry": "776x779+1604+284"
|
||||||
|
},
|
||||||
"medikament_quelle": "compendium.ch",
|
"medikament_quelle": "compendium.ch",
|
||||||
"diagnose_quelle": "",
|
"diagnose_quelle": "",
|
||||||
"dokumente_collapsed": false,
|
"dokumente_collapsed": false,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"⏺ Start": 33,
|
"⏺ Start": 23,
|
||||||
"Übersetzen": 1
|
"🔑": 2,
|
||||||
|
"👤": 6,
|
||||||
|
"⏺ Aufnahme starten": 5,
|
||||||
|
"Diktat": 1
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
420x380+1889+452
|
420x380+1744+1053
|
||||||
@@ -1 +1 @@
|
|||||||
908x1041+1071+79
|
908x1041+1564+540
|
||||||
@@ -1 +1 @@
|
|||||||
{"used": 521654, "total": 1000000, "budget_dollars": 0, "used_dollars": 0}
|
{"used": 522567, "total": 1000000, "budget_dollars": 0, "used_dollars": 0}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "André M. Surovy",
|
"name": "André M. Surovy",
|
||||||
"specialty": "Dermatologie",
|
"specialty": "Dermatologie",
|
||||||
"clinic": "Praxis Lindegut AG"
|
"clinic": "Praxis Lindegut AG",
|
||||||
|
"code": "",
|
||||||
|
"email": "andre.surovy@haut-winterthur.ch",
|
||||||
|
"password_hash": "$2b$12$VdOUv97Tzk7ccr2HsU632.5L5waHju1YDmw6oJnRkTWcpp/E6lBbW",
|
||||||
|
"license_key": "AZA-6TY3-63AU-W9ZR-ZO7D",
|
||||||
|
"practice_id": "prac_189854535b85"
|
||||||
}
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
1461 1205 1189 477 638 340
|
1461 1205 1189 477 717 404
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"valid": true, "valid_until": 1777652509, "cached_at": 1775118734.4902344}
|
{"valid": true, "valid_until": 1776783700, "cached_at": 1776697300.917711}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"transcript_vertical": 340,
|
"transcript_vertical": 404,
|
||||||
"kg_vertical": 329
|
"kg_vertical": 535
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ import smtplib
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
import string
|
import string
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
@@ -115,6 +116,8 @@ def _ensure_storage() -> None:
|
|||||||
con.execute("ALTER TABLE licenses ADD COLUMN current_period_end INTEGER")
|
con.execute("ALTER TABLE licenses ADD COLUMN current_period_end INTEGER")
|
||||||
if "license_key" not in cols:
|
if "license_key" not in cols:
|
||||||
con.execute("ALTER TABLE licenses ADD COLUMN license_key TEXT")
|
con.execute("ALTER TABLE licenses ADD COLUMN license_key TEXT")
|
||||||
|
if "practice_id" not in cols:
|
||||||
|
con.execute("ALTER TABLE licenses ADD COLUMN practice_id TEXT")
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
|
|
||||||
@@ -409,6 +412,59 @@ def _log_event(kind: str, payload: Dict[str, Any]) -> None:
|
|||||||
f.write(json.dumps(rec, ensure_ascii=False, default=_decimal_default) + "\n")
|
f.write(json.dumps(rec, ensure_ascii=False, default=_decimal_default) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _new_practice_id() -> str:
|
||||||
|
"""Gleiches Format wie empfang_routes._generate_practice_id (Mandanten-ID)."""
|
||||||
|
return f"prac_{uuid.uuid4().hex[:12]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_empfang_practice_from_license(
|
||||||
|
practice_id: str,
|
||||||
|
customer_email: Optional[str],
|
||||||
|
display_name: str,
|
||||||
|
) -> None:
|
||||||
|
"""Empfang-Praxisdatei mit SQLite-Lizenz synchronisieren (eine Wahrheit)."""
|
||||||
|
pid = (practice_id or "").strip()
|
||||||
|
if not pid:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from empfang_routes import _ensure_practice
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[STRIPE] empfang_routes Import: {exc}")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
em = (customer_email or "").strip()
|
||||||
|
name = (display_name or "").strip() or (em.split("@")[0] if "@" in em else "Meine Praxis")
|
||||||
|
_ensure_practice(pid, name=name, admin_email=em)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[STRIPE] _ensure_practice: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
def lookup_practice_id_for_license_email(email: str) -> Optional[str]:
|
||||||
|
"""Liefert die serverseitig gespeicherte practice_id zur Kunden-E-Mail (Lizenz ↔ Praxis)."""
|
||||||
|
_ensure_storage()
|
||||||
|
e = (email or "").strip().lower()
|
||||||
|
if not e:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(DB_PATH) as con:
|
||||||
|
row = con.execute(
|
||||||
|
"""
|
||||||
|
SELECT practice_id FROM licenses
|
||||||
|
WHERE lower(customer_email) = ?
|
||||||
|
AND practice_id IS NOT NULL
|
||||||
|
AND trim(practice_id) != ''
|
||||||
|
ORDER BY updated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(e,),
|
||||||
|
).fetchone()
|
||||||
|
if not row or not row[0]:
|
||||||
|
return None
|
||||||
|
return str(row[0]).strip()
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _upsert_license(
|
def _upsert_license(
|
||||||
*,
|
*,
|
||||||
subscription_id: str,
|
subscription_id: str,
|
||||||
@@ -422,27 +478,36 @@ def _upsert_license(
|
|||||||
current_period_end: Optional[int],
|
current_period_end: Optional[int],
|
||||||
license_key: Optional[str] = None,
|
license_key: Optional[str] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Upsert license row. Returns the license_key (generated if not yet set)."""
|
"""Upsert license row. Returns the license_key (generated if not yet set).
|
||||||
|
|
||||||
|
Jede Lizenzzeile erhält spätestens hier eine stabile practice_id (Mandant),
|
||||||
|
damit Stripe-Webhook und Desktop/Empfang dieselbe ID nutzen.
|
||||||
|
"""
|
||||||
now = int(time.time())
|
now = int(time.time())
|
||||||
with sqlite3.connect(DB_PATH) as con:
|
with sqlite3.connect(DB_PATH) as con:
|
||||||
existing_key = None
|
existing_key = None
|
||||||
|
existing_pid = ""
|
||||||
row = con.execute(
|
row = con.execute(
|
||||||
"SELECT license_key FROM licenses WHERE subscription_id = ?",
|
"SELECT license_key, practice_id FROM licenses WHERE subscription_id = ?",
|
||||||
(subscription_id,),
|
(subscription_id,),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row and row[0]:
|
if row:
|
||||||
existing_key = row[0]
|
if row[0]:
|
||||||
|
existing_key = row[0]
|
||||||
|
if row[1]:
|
||||||
|
existing_pid = str(row[1]).strip()
|
||||||
|
|
||||||
final_key = existing_key or license_key or _generate_license_key()
|
final_key = existing_key or license_key or _generate_license_key()
|
||||||
|
final_pid = existing_pid or _new_practice_id()
|
||||||
|
|
||||||
con.execute(
|
con.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO licenses(
|
INSERT INTO licenses(
|
||||||
subscription_id, customer_id, status, lookup_key,
|
subscription_id, customer_id, status, lookup_key,
|
||||||
allowed_users, devices_per_user, customer_email, client_reference_id,
|
allowed_users, devices_per_user, customer_email, client_reference_id,
|
||||||
current_period_end, updated_at, license_key
|
current_period_end, updated_at, license_key, practice_id
|
||||||
)
|
)
|
||||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
ON CONFLICT(subscription_id) DO UPDATE SET
|
ON CONFLICT(subscription_id) DO UPDATE SET
|
||||||
customer_id=excluded.customer_id,
|
customer_id=excluded.customer_id,
|
||||||
status=excluded.status,
|
status=excluded.status,
|
||||||
@@ -453,7 +518,12 @@ def _upsert_license(
|
|||||||
client_reference_id=COALESCE(excluded.client_reference_id, client_reference_id),
|
client_reference_id=COALESCE(excluded.client_reference_id, client_reference_id),
|
||||||
current_period_end=COALESCE(excluded.current_period_end, current_period_end),
|
current_period_end=COALESCE(excluded.current_period_end, current_period_end),
|
||||||
updated_at=excluded.updated_at,
|
updated_at=excluded.updated_at,
|
||||||
license_key=COALESCE(license_key, excluded.license_key)
|
license_key=COALESCE(license_key, excluded.license_key),
|
||||||
|
practice_id=CASE
|
||||||
|
WHEN licenses.practice_id IS NOT NULL AND trim(licenses.practice_id) != ''
|
||||||
|
THEN licenses.practice_id
|
||||||
|
ELSE excluded.practice_id
|
||||||
|
END
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
subscription_id,
|
subscription_id,
|
||||||
@@ -467,9 +537,21 @@ def _upsert_license(
|
|||||||
current_period_end,
|
current_period_end,
|
||||||
now,
|
now,
|
||||||
final_key,
|
final_key,
|
||||||
|
final_pid,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
con.commit()
|
con.commit()
|
||||||
|
|
||||||
|
row2 = con.execute(
|
||||||
|
"SELECT practice_id, customer_email FROM licenses WHERE subscription_id = ?",
|
||||||
|
(subscription_id,),
|
||||||
|
).fetchone()
|
||||||
|
if row2:
|
||||||
|
pid_s = str(row2[0]).strip() if row2[0] else ""
|
||||||
|
em = (str(row2[1]).strip() if row2[1] else "") or (customer_email or "").strip()
|
||||||
|
if pid_s:
|
||||||
|
disp = em.split("@")[0] if "@" in em else "Meine Praxis"
|
||||||
|
_sync_empfang_practice_from_license(pid_s, em or None, disp)
|
||||||
return final_key
|
return final_key
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -183,6 +183,19 @@ header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowra
|
|||||||
.login-switch{text-align:center;margin-top:14px;font-size:.82rem;color:#6a8a9a}
|
.login-switch{text-align:center;margin-top:14px;font-size:.82rem;color:#6a8a9a}
|
||||||
.login-switch a{color:#5B8DB3;cursor:pointer;text-decoration:underline}
|
.login-switch a{color:#5B8DB3;cursor:pointer;text-decoration:underline}
|
||||||
|
|
||||||
|
/* Registrierung: Überschrift, Fließtext, Labels, Felder, Button, Link – alles 9pt */
|
||||||
|
.login-box.login-register,
|
||||||
|
.login-box.login-register h2,
|
||||||
|
.login-box.login-register p,
|
||||||
|
.login-box.login-register label,
|
||||||
|
.login-box.login-register input,
|
||||||
|
.login-box.login-register select,
|
||||||
|
.login-box.login-register button,
|
||||||
|
.login-box.login-register .login-error,
|
||||||
|
.login-box.login-register .login-switch,
|
||||||
|
.login-box.login-register .login-switch a{font-size:9pt!important}
|
||||||
|
.login-box.login-register h2{font-weight:600}
|
||||||
|
|
||||||
/* === Admin Panel === */
|
/* === Admin Panel === */
|
||||||
.admin-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.3);z-index:150;align-items:flex-start;justify-content:center;padding-top:40px;overflow-y:auto}
|
.admin-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.3);z-index:150;align-items:flex-start;justify-content:center;padding-top:40px;overflow-y:auto}
|
||||||
.admin-overlay.open{display:flex}
|
.admin-overlay.open{display:flex}
|
||||||
@@ -386,7 +399,7 @@ header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowra
|
|||||||
|
|
||||||
<div class="status-bar">
|
<div class="status-bar">
|
||||||
<span>Aktualisiert alle 10 Sek.</span>
|
<span>Aktualisiert alle 10 Sek.</span>
|
||||||
<span style="opacity:.5" id="ui-version">v2026.04.18b</span>
|
<span style="opacity:.5" id="ui-version">v2026.04.20</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -394,6 +407,9 @@ header h1{font-size:1.2rem;font-weight:600;letter-spacing:.3px;white-space:nowra
|
|||||||
STATE
|
STATE
|
||||||
=================================================================== */
|
=================================================================== */
|
||||||
var API_BASE = window.location.origin + '/empfang';
|
var API_BASE = window.location.origin + '/empfang';
|
||||||
|
function getPracticeIdOrEmpty() {
|
||||||
|
try { return localStorage.getItem('aza_practice_id') || ''; } catch (e) { return ''; }
|
||||||
|
}
|
||||||
var currentSession = null;
|
var currentSession = null;
|
||||||
var practiceUsers = [];
|
var practiceUsers = [];
|
||||||
var serverTasks = [];
|
var serverTasks = [];
|
||||||
@@ -443,7 +459,9 @@ var currentToneIdx = parseInt(localStorage.getItem('empfang_tone_idx') || '0', 1
|
|||||||
API WRAPPER (global 401 handling)
|
API WRAPPER (global 401 handling)
|
||||||
=================================================================== */
|
=================================================================== */
|
||||||
async function apiFetch(url, opts) {
|
async function apiFetch(url, opts) {
|
||||||
var r = await fetch(url, opts || {});
|
opts = opts || {};
|
||||||
|
if (!opts.credentials) opts.credentials = 'include';
|
||||||
|
var r = await fetch(url, opts);
|
||||||
if (r.status === 401) {
|
if (r.status === 401) {
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
stopPolling();
|
stopPolling();
|
||||||
@@ -458,7 +476,7 @@ async function apiFetch(url, opts) {
|
|||||||
=================================================================== */
|
=================================================================== */
|
||||||
async function checkAuth() {
|
async function checkAuth() {
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/me');
|
var r = await fetch(API_BASE + '/auth/me', {credentials: 'include'});
|
||||||
if (r.status === 401) return null;
|
if (r.status === 401) return null;
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
if (d.authenticated) return d;
|
if (d.authenticated) return d;
|
||||||
@@ -471,7 +489,7 @@ async function showLoginOverlay() {
|
|||||||
var overlay = document.getElementById('login-overlay');
|
var overlay = document.getElementById('login-overlay');
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/needs_setup');
|
var r = await fetch(API_BASE + '/auth/needs_setup', {credentials: 'include'});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
if (d.needs_setup) { renderSetupForm(); return; }
|
if (d.needs_setup) { renderSetupForm(); return; }
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
@@ -484,6 +502,7 @@ function hideLoginOverlay() {
|
|||||||
|
|
||||||
function renderSetupForm() {
|
function renderSetupForm() {
|
||||||
var box = document.getElementById('login-box');
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<h2>AZA Praxis-Chat einrichten</h2>' +
|
'<h2>AZA Praxis-Chat einrichten</h2>' +
|
||||||
'<p>Willkommen! Richten Sie Ihre Praxis und den ersten Administrator ein.</p>' +
|
'<p>Willkommen! Richten Sie Ihre Praxis und den ersten Administrator ein.</p>' +
|
||||||
@@ -498,6 +517,7 @@ function renderSetupForm() {
|
|||||||
|
|
||||||
function renderLoginForm() {
|
function renderLoginForm() {
|
||||||
var box = document.getElementById('login-box');
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
var lastUser = localStorage.getItem('aza_last_login_user') || '';
|
var lastUser = localStorage.getItem('aza_last_login_user') || '';
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<h2>Anmelden</h2>' +
|
'<h2>Anmelden</h2>' +
|
||||||
@@ -520,39 +540,159 @@ function renderLoginForm() {
|
|||||||
|
|
||||||
function renderForgotPasswordForm() {
|
function renderForgotPasswordForm() {
|
||||||
var box = document.getElementById('login-box');
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
|
var pid = getPracticeIdOrEmpty();
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<h2>Passwort vergessen</h2>' +
|
'<h2>Passwort vergessen</h2>' +
|
||||||
'<p>Geben Sie Ihre E-Mail-Adresse ein. Sie erhalten einen Link zum Zur\u00fccksetzen.</p>' +
|
'<p>Geben Sie Ihren <strong>Benutzernamen</strong> (wie in der Praxis angezeigt) oder Ihre <strong>E-Mail-Adresse</strong> ein.</p>' +
|
||||||
'<div class="login-field"><label>E-Mail</label><input type="email" id="forgot-email" autocomplete="email" placeholder="praxis@beispiel.ch"></div>' +
|
'<p style="font-size:.82rem;color:#6a8a9a">Nach einer erfolgreichen Anmeldung speichert der Browser die Praxiszuordnung — dann ist der Ablauf einfacher.</p>' +
|
||||||
|
(pid ? '' : '<p style="font-size:.78rem;color:#a67c00">Hinweis: Auf diesem Ger\u00e4t ist noch keine Praxis gespeichert. Benutzername oder E-Mail trotzdem m\u00f6glich; bei mehreren Konten ggf. zus\u00e4tzliche Auswahl.</p>') +
|
||||||
|
'<div class="login-field"><label>Benutzername oder E-Mail</label><input type="text" id="forgot-login" autocomplete="username" placeholder="z. B. Suro oder name@praxis.ch"></div>' +
|
||||||
'<button class="login-btn" onclick="doForgotPassword()">Reset-Link senden</button>' +
|
'<button class="login-btn" onclick="doForgotPassword()">Reset-Link senden</button>' +
|
||||||
'<div class="login-error" id="login-error"></div>' +
|
'<div class="login-error" id="login-error"></div>' +
|
||||||
'<div class="login-switch"><a onclick="renderLoginForm()">← Zur\u00fcck zur Anmeldung</a></div>';
|
'<div class="login-switch"><a onclick="renderLoginForm()">← Zur\u00fcck zur Anmeldung</a></div>';
|
||||||
document.getElementById('forgot-email').focus();
|
document.getElementById('forgot-login').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderForgotPickUser(loginEmail, candidates) {
|
||||||
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
|
var buttons = (candidates || []).map(function(c, i) {
|
||||||
|
var lab = esc(c.display_name || '') + (c.practice_name ? ' \u2013 ' + esc(c.practice_name) : '');
|
||||||
|
return '<button type="button" class="login-btn" style="margin-bottom:8px;width:100%;text-align:left" onclick="doForgotPickCandidate(' + i + ')">' + lab + '</button>';
|
||||||
|
}).join('');
|
||||||
|
box.innerHTML =
|
||||||
|
'<h2>Passwort vergessen</h2>' +
|
||||||
|
'<p>Diese E-Mail-Adresse ist <strong>mehreren Benutzerkonten</strong> zugeordnet. Bitte w\u00e4hlen Sie Ihr Konto oder geben Sie Ihren Benutzernamen ein.</p>' +
|
||||||
|
'<div class="login-field" style="margin-top:10px">' + buttons + '</div>' +
|
||||||
|
'<div class="login-field"><label>Benutzername (exakt)</label><input type="text" id="forgot-pick-manual" autocomplete="username" placeholder="Wie in der Praxis angezeigt"></div>' +
|
||||||
|
'<button class="login-btn" onclick="doForgotPickManual()">Auswahl best\u00e4tigen</button>' +
|
||||||
|
'<div class="login-error" id="login-error"></div>' +
|
||||||
|
'<div class="login-switch"><a onclick="renderForgotPasswordForm()">← Zur\u00fcck</a></div>';
|
||||||
|
window._forgotCandidates = candidates;
|
||||||
|
window._forgotLoginEmail = loginEmail;
|
||||||
|
document.getElementById('forgot-pick-manual').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doForgotPickCandidate(i) {
|
||||||
|
var c = (window._forgotCandidates || [])[i];
|
||||||
|
if (!c) return;
|
||||||
|
await _postForgotSecondStep(window._forgotLoginEmail, c.display_name, c.practice_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function doForgotPickManual() {
|
||||||
|
var v = (document.getElementById('forgot-pick-manual').value || '').trim();
|
||||||
|
var errEl = document.getElementById('login-error');
|
||||||
|
var cand = window._forgotCandidates || [];
|
||||||
|
var hit = null;
|
||||||
|
for (var j = 0; j < cand.length; j++) {
|
||||||
|
if ((cand[j].display_name || '') === v) { hit = cand[j]; break; }
|
||||||
|
}
|
||||||
|
if (!hit) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = 'Kein passender Benutzername. Bitte exakt wie in der Praxis w\u00e4hlen oder eingeben.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await _postForgotSecondStep(window._forgotLoginEmail, hit.display_name, hit.practice_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _postForgotSecondStep(loginEmail, displayName, practiceId) {
|
||||||
|
var errEl = document.getElementById('login-error');
|
||||||
|
errEl.style.color = '';
|
||||||
|
errEl.textContent = '';
|
||||||
|
try {
|
||||||
|
var r = await fetch(API_BASE + '/auth/forgot_password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
login: loginEmail,
|
||||||
|
display_name: displayName,
|
||||||
|
practice_id: practiceId || getPracticeIdOrEmpty()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
var d = await r.json().catch(function() { return {}; });
|
||||||
|
if (d.step === 'no_email' || (d.success === false && d.step === 'no_email')) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = d.message || 'Keine E-Mail hinterlegt.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.success === false && d.message) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = d.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
errEl.style.color = '#155724';
|
||||||
|
errEl.textContent = d.message || 'Link wurde gesendet.';
|
||||||
|
} catch (e) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = 'Fehler beim Senden.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doForgotPassword() {
|
async function doForgotPassword() {
|
||||||
var email = (document.getElementById('forgot-email').value || '').trim();
|
var raw = (document.getElementById('forgot-login').value || '').trim();
|
||||||
var errEl = document.getElementById('login-error');
|
var errEl = document.getElementById('login-error');
|
||||||
if (!email) { errEl.textContent = 'Bitte E-Mail-Adresse eingeben.'; return; }
|
errEl.style.color = '';
|
||||||
|
if (!raw) { errEl.style.color = '#842029'; errEl.textContent = 'Bitte Benutzername oder E-Mail eingeben.'; return; }
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/forgot_password', {
|
var r = await fetch(API_BASE + '/auth/forgot_password', {
|
||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST',
|
||||||
body: JSON.stringify({email: email})
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({login: raw, practice_id: getPracticeIdOrEmpty()})
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json().catch(function() { return {}; });
|
||||||
|
if (d.step === 'pick_user' && d.candidates && d.candidates.length) {
|
||||||
|
renderForgotPickUser(d.login || raw, d.candidates);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.step === 'no_email' || (d.success === false && d.step === 'no_email')) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = d.message || 'Keine E-Mail hinterlegt.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d.success === false && d.message) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = d.message;
|
||||||
|
return;
|
||||||
|
}
|
||||||
errEl.style.color = '#155724';
|
errEl.style.color = '#155724';
|
||||||
errEl.textContent = d.message || 'Reset-Link wurde gesendet.';
|
errEl.textContent = d.message || 'Wenn ein passendes Konto existiert, wurde ein Link gesendet.';
|
||||||
} catch(e) { errEl.textContent = 'Fehler beim Senden.'; }
|
} catch(e) {
|
||||||
|
errEl.style.color = '#842029';
|
||||||
|
errEl.textContent = 'Fehler beim Senden.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripResetTokenFromUrl() {
|
||||||
|
try {
|
||||||
|
var p = window.location.pathname || '';
|
||||||
|
var sp = new URLSearchParams(window.location.search);
|
||||||
|
sp.delete('reset_token');
|
||||||
|
var q = sp.toString();
|
||||||
|
history.replaceState(null, '', p + (q ? '?' + q : ''));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResetPasswordInvalid(msg) {
|
||||||
|
stripResetTokenFromUrl();
|
||||||
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
|
box.innerHTML =
|
||||||
|
'<h2>Link ung\u00fcltig</h2>' +
|
||||||
|
'<p class="login-error" id="login-error" style="display:block;color:#842029">' + esc(msg || 'Ung\u00fcltiger oder abgelaufener Link.') + '</p>' +
|
||||||
|
'<div class="login-switch"><a onclick="renderLoginForm()">Zur Anmeldung</a></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResetPasswordForm(resetToken) {
|
function renderResetPasswordForm(resetToken) {
|
||||||
var box = document.getElementById('login-box');
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box';
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<h2>Neues Passwort setzen</h2>' +
|
'<h2>Passwort zur\u00fccksetzen</h2>' +
|
||||||
|
'<p>Sie haben eine Passwort-Zur\u00fccksetzung angefordert.</p>' +
|
||||||
'<p>Bitte w\u00e4hlen Sie ein neues Passwort (min. 4 Zeichen).</p>' +
|
'<p>Bitte w\u00e4hlen Sie ein neues Passwort (min. 4 Zeichen).</p>' +
|
||||||
'<div class="login-field"><label>Neues Passwort</label><input type="password" id="reset-pass" autocomplete="new-password"></div>' +
|
'<div class="login-field"><label>Neues Passwort</label><input type="password" id="reset-pass" autocomplete="new-password"></div>' +
|
||||||
'<div class="login-field"><label>Passwort best\u00e4tigen</label><input type="password" id="reset-pass2" autocomplete="new-password"></div>' +
|
'<div class="login-field"><label>Neues Passwort best\u00e4tigen</label><input type="password" id="reset-pass2" autocomplete="new-password"></div>' +
|
||||||
'<button class="login-btn" onclick="doResetPassword(\'' + resetToken + '\')">Passwort speichern</button>' +
|
'<button class="login-btn" onclick="doResetPassword(\'' + resetToken + '\')">Neues Passwort speichern</button>' +
|
||||||
'<div class="login-error" id="login-error"></div>';
|
'<div class="login-error" id="login-error"></div>';
|
||||||
document.getElementById('reset-pass').focus();
|
document.getElementById('reset-pass').focus();
|
||||||
}
|
}
|
||||||
@@ -568,23 +708,34 @@ async function doResetPassword(resetToken) {
|
|||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({reset_token: resetToken, password: pass1})
|
body: JSON.stringify({reset_token: resetToken, password: pass1})
|
||||||
});
|
});
|
||||||
var d = await r.json();
|
var d = await r.json().catch(function() { return {}; });
|
||||||
if (r.ok && d.success) {
|
if (r.ok && d.success) {
|
||||||
|
stripResetTokenFromUrl();
|
||||||
|
var pref = (d.display_name || d.email || '').trim();
|
||||||
|
if (pref) { try { localStorage.setItem('aza_last_login_user', pref); } catch (e2) {} }
|
||||||
errEl.style.color = '#155724';
|
errEl.style.color = '#155724';
|
||||||
errEl.textContent = 'Passwort ge\u00e4ndert. Sie k\u00f6nnen sich jetzt anmelden.';
|
errEl.textContent = 'Passwort wurde erfolgreich ge\u00e4ndert. Sie k\u00f6nnen sich jetzt anmelden.';
|
||||||
setTimeout(renderLoginForm, 2000);
|
setTimeout(renderLoginForm, 2000);
|
||||||
} else {
|
} else {
|
||||||
errEl.textContent = d.detail || d.message || 'Fehler beim Zur\u00fccksetzen.';
|
var fail = d.detail;
|
||||||
|
if (typeof fail === 'string') {
|
||||||
|
errEl.textContent = fail;
|
||||||
|
} else if (Array.isArray(fail) && fail[0] && fail[0].msg) {
|
||||||
|
errEl.textContent = fail.map(function(x) { return x.msg; }).join(' ');
|
||||||
|
} else {
|
||||||
|
errEl.textContent = d.message || 'Passwort konnte nicht zur\u00fcckgesetzt werden.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(e) { errEl.textContent = 'Fehler beim Zur\u00fccksetzen.'; }
|
} catch(e) { errEl.textContent = 'Passwort konnte nicht zur\u00fcckgesetzt werden.'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRegisterForm() {
|
function renderRegisterForm() {
|
||||||
var box = document.getElementById('login-box');
|
var box = document.getElementById('login-box');
|
||||||
|
box.className = 'login-box login-register';
|
||||||
box.innerHTML =
|
box.innerHTML =
|
||||||
'<h2>Bei Ihrer Praxis registrieren</h2>' +
|
'<h2>registrieren Sie sich f\u00fcr den Chat</h2>' +
|
||||||
'<p>Ihr Administrator hat Ihnen einen Einladungscode gegeben.</p>' +
|
'<p>Ihr Administrator hat Ihnen einen Einladungscode gegeben.</p>' +
|
||||||
'<div class="login-field"><label>Einladungscode</label><input type="text" id="reg-code" placeholder="z.B. Xk7m-9pQr"></div>' +
|
'<div class="login-field"><label>Einladungscode</label><input type="text" id="reg-code" placeholder="z.B. CHAT-AB12-CD34"></div>' +
|
||||||
'<div class="login-field"><label>Ihr Name</label><input type="text" id="reg-name" autocomplete="username" placeholder="z.B. Sandra M\u00fcller"></div>' +
|
'<div class="login-field"><label>Ihr Name</label><input type="text" id="reg-name" autocomplete="username" placeholder="z.B. Sandra M\u00fcller"></div>' +
|
||||||
'<div class="login-field"><label>Passwort (min. 4 Zeichen)</label><input type="password" id="reg-pass" autocomplete="new-password"></div>' +
|
'<div class="login-field"><label>Passwort (min. 4 Zeichen)</label><input type="password" id="reg-pass" autocomplete="new-password"></div>' +
|
||||||
'<div class="login-field"><label>Ihre Rolle</label>' +
|
'<div class="login-field"><label>Ihre Rolle</label>' +
|
||||||
@@ -625,10 +776,15 @@ async function doSetup() {
|
|||||||
if (!pass || pass.length < 4) { errEl.textContent = 'Passwort (min. 4 Zeichen) erforderlich.'; return; }
|
if (!pass || pass.length < 4) { errEl.textContent = 'Passwort (min. 4 Zeichen) erforderlich.'; return; }
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/setup', {
|
var r = await fetch(API_BASE + '/auth/setup', {
|
||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', credentials: 'include',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({name: name, password: pass, practice_name: practiceName, email: email})
|
body: JSON.stringify({name: name, password: pass, practice_name: practiceName, email: email})
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
try {
|
||||||
|
var sd = await r.json();
|
||||||
|
if (sd.practice_id) localStorage.setItem('aza_practice_id', sd.practice_id);
|
||||||
|
} catch(e) {}
|
||||||
if (!await onAuthSuccess()) errEl.textContent = 'Einrichtung fehlgeschlagen.';
|
if (!await onAuthSuccess()) errEl.textContent = 'Einrichtung fehlgeschlagen.';
|
||||||
} else {
|
} else {
|
||||||
var d = await r.json().catch(function(){ return {}; });
|
var d = await r.json().catch(function(){ return {}; });
|
||||||
@@ -641,20 +797,39 @@ async function doLogin() {
|
|||||||
var name = (document.getElementById('login-name').value || '').trim();
|
var name = (document.getElementById('login-name').value || '').trim();
|
||||||
var pass = document.getElementById('login-pass').value || '';
|
var pass = document.getElementById('login-pass').value || '';
|
||||||
var errEl = document.getElementById('login-error');
|
var errEl = document.getElementById('login-error');
|
||||||
if (!name || !pass) { errEl.textContent = 'Name/E-Mail und Passwort erforderlich.'; return; }
|
errEl.style.color = '';
|
||||||
|
if (!name || !pass) { errEl.textContent = 'Benutzername/E-Mail und Passwort erforderlich.'; return; }
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/login', {
|
var r = await fetch(API_BASE + '/auth/login', {
|
||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', credentials: 'include',
|
||||||
body: JSON.stringify({name: name, password: pass})
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: name,
|
||||||
|
password: pass,
|
||||||
|
practice_id: getPracticeIdOrEmpty()
|
||||||
|
})
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
localStorage.setItem('aza_last_login_user', name);
|
try {
|
||||||
|
var ld = await r.json();
|
||||||
|
if (ld.practice_id) localStorage.setItem('aza_practice_id', ld.practice_id);
|
||||||
|
var saveUser = (ld.display_name || name).trim();
|
||||||
|
localStorage.setItem('aza_last_login_user', saveUser);
|
||||||
|
} catch(e) {}
|
||||||
if (!await onAuthSuccess()) errEl.textContent = 'Anmeldung fehlgeschlagen.';
|
if (!await onAuthSuccess()) errEl.textContent = 'Anmeldung fehlgeschlagen.';
|
||||||
} else {
|
} else {
|
||||||
var d = await r.json().catch(function(){ return {}; });
|
var d = await r.json().catch(function(){ return {}; });
|
||||||
errEl.textContent = d.detail || d.error || 'Anmeldung fehlgeschlagen.';
|
errEl.style.color = '#842029';
|
||||||
|
var det = d.detail;
|
||||||
|
if (typeof det === 'object' && det !== null && det.message) {
|
||||||
|
errEl.textContent = det.message;
|
||||||
|
} else if (typeof det === 'string') {
|
||||||
|
errEl.textContent = det;
|
||||||
|
} else {
|
||||||
|
errEl.textContent = d.error || 'Anmeldung fehlgeschlagen.';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch(e) { errEl.textContent = 'Verbindungsfehler.'; }
|
} catch(e) { errEl.style.color = '#842029'; errEl.textContent = 'Verbindungsfehler.'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doRegister() {
|
async function doRegister() {
|
||||||
@@ -666,10 +841,15 @@ async function doRegister() {
|
|||||||
if (!code || !name || !pass) { errEl.textContent = 'Alle Felder erforderlich.'; return; }
|
if (!code || !name || !pass) { errEl.textContent = 'Alle Felder erforderlich.'; return; }
|
||||||
try {
|
try {
|
||||||
var r = await fetch(API_BASE + '/auth/register', {
|
var r = await fetch(API_BASE + '/auth/register', {
|
||||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
method: 'POST', credentials: 'include',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
body: JSON.stringify({invite_code: code, name: name, password: pass, role: role})
|
body: JSON.stringify({invite_code: code, name: name, password: pass, role: role})
|
||||||
});
|
});
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
|
try {
|
||||||
|
var rd = await r.json();
|
||||||
|
if (rd.practice_id) localStorage.setItem('aza_practice_id', rd.practice_id);
|
||||||
|
} catch(e) {}
|
||||||
if (!await onAuthSuccess()) errEl.textContent = 'Registrierung fehlgeschlagen.';
|
if (!await onAuthSuccess()) errEl.textContent = 'Registrierung fehlgeschlagen.';
|
||||||
} else {
|
} else {
|
||||||
var d = await r.json().catch(function(){ return {}; });
|
var d = await r.json().catch(function(){ return {}; });
|
||||||
@@ -679,7 +859,7 @@ async function doRegister() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function doLogout() {
|
async function doLogout() {
|
||||||
try { await fetch(API_BASE + '/auth/logout', {method: 'POST'}); } catch(e) {}
|
try { await fetch(API_BASE + '/auth/logout', {method: 'POST', credentials: 'include'}); } catch(e) {}
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
practiceUsers = [];
|
practiceUsers = [];
|
||||||
serverTasks = [];
|
serverTasks = [];
|
||||||
@@ -721,11 +901,26 @@ async function doLogout() {
|
|||||||
if (resetToken) {
|
if (resetToken) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
document.getElementById('login-overlay').classList.remove('hidden');
|
document.getElementById('login-overlay').classList.remove('hidden');
|
||||||
renderResetPasswordForm(resetToken);
|
try {
|
||||||
|
var vr = await fetch(
|
||||||
|
API_BASE + '/auth/reset_verify?reset_token=' + encodeURIComponent(resetToken)
|
||||||
|
);
|
||||||
|
var vd = await vr.json().catch(function() { return {valid: false}; });
|
||||||
|
if (!vd.valid) {
|
||||||
|
renderResetPasswordInvalid(vd.detail || 'Ung\u00fcltiger oder abgelaufener Link.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderResetPasswordForm(resetToken);
|
||||||
|
} catch (e) {
|
||||||
|
renderResetPasswordInvalid('Verbindungsfehler. Bitte sp\u00e4ter erneut versuchen.');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var me = await checkAuth();
|
var me = await checkAuth();
|
||||||
|
if (me && me.practice_id) {
|
||||||
|
try { localStorage.setItem('aza_practice_id', me.practice_id); } catch(e) {}
|
||||||
|
}
|
||||||
if (!me) {
|
if (!me) {
|
||||||
var inviteParam = urlParams.get('invite');
|
var inviteParam = urlParams.get('invite');
|
||||||
stopPolling();
|
stopPolling();
|
||||||
@@ -1629,7 +1824,7 @@ async function activateUser(userId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function resetPassword(userId, userName) {
|
async function resetPassword(userId, userName) {
|
||||||
if (!confirm('Passwort von "' + userName + '" zuruecksetzen?')) return;
|
if (!confirm('Passwort von "' + userName + '" zur\u00fccksetzen?')) return;
|
||||||
try {
|
try {
|
||||||
var r = await apiFetch(API_BASE + '/admin/users/' + userId + '/reset_password', {method:'POST'});
|
var r = await apiFetch(API_BASE + '/admin/users/' + userId + '/reset_password', {method:'POST'});
|
||||||
var d = await r.json();
|
var d = await r.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user