Personalisierung Schritt 2: Aktivierungscode, Einreichen, Bestätigen, E-Mail-Domänen (Migration 0065)
Code je Gerät (7 Tage, nur Hash gespeichert), Einreichen mit Domänen- und Anmeldenamenprüfung (Passwort kommt nur als SHA-512-crypt), Bestätigung binnen einer Stunde, verfallene Vorgänge verlieren Hash und Adresse. Die Platzhalterseite /personalisierung entfällt, das Formular liegt in der Kundenplattform. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90586094f0
commit
327bf948df
2 files changed
+471
-27
No files matched your search
@@ -23,7 +23,7 @@ from cryptography.fernet import Fernet, InvalidToken
|
||||
|
||||
from fastapi import FastAPI, Header, HTTPException, Request
|
||||
from pydantic import BaseModel
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from fastapi.responses import FileResponse
|
||||
|
||||
|
||||
LOG_PATH = Path("activation.log")
|
||||
@@ -3806,7 +3806,8 @@ def fetch_organization(organization_id: str):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT o.id, o.name, o.created_at, o.kontakt_name, o.adresse, o.telefon,
|
||||
o.kontakt_email, o.notizen, o.ausrollstufe, o.kundengruppe_id, k.name
|
||||
o.kontakt_email, o.notizen, o.ausrollstufe, o.kundengruppe_id, k.name,
|
||||
o.email_domaenen
|
||||
FROM organizations o
|
||||
LEFT JOIN kundengruppen k ON k.id = o.kundengruppe_id
|
||||
WHERE o.id = %s
|
||||
@@ -3830,6 +3831,7 @@ def fetch_organization(organization_id: str):
|
||||
"ausrollstufe": row[8],
|
||||
"kundengruppe_id": str(row[9]) if row[9] else None,
|
||||
"kundengruppe_name": row[10],
|
||||
"email_domaenen": list(row[11] or []),
|
||||
}
|
||||
|
||||
def create_organization(name: str, kundengruppe_id: str | None = None):
|
||||
@@ -5822,33 +5824,423 @@ def health():
|
||||
}
|
||||
|
||||
|
||||
# Startseite des Kiosks im Konto "Personalisierung" (ADR-0041, Rolle
|
||||
# personalisierung in ansible-content). Bis das Formular mit Aktivierungscode
|
||||
# steht, nur ein Hinweis. "tuxflotte:abmelden" fängt der Kiosk ab und beendet
|
||||
# die Sitzung.
|
||||
PERSONALISIERUNG_SEITE = """<!doctype html>
|
||||
<html lang="de"><head><meta charset="utf-8">
|
||||
<title>Personalisierung</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; background: #f4f4f4; color: #222;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
height: 100vh; margin: 0; }
|
||||
main { max-width: 34em; text-align: center; }
|
||||
a { display: inline-block; margin: 1em .5em; padding: .7em 1.4em;
|
||||
background: #2a6db0; color: #fff; text-decoration: none; border-radius: 4px; }
|
||||
</style></head><body><main>
|
||||
<h1>Personalisierung</h1>
|
||||
<p>Hier wird bald ein persönliches Konto für dieses Gerät eingerichtet.
|
||||
Dafür brauchst du einen Aktivierungscode von der Verwaltung deiner
|
||||
Einrichtung. Die Einrichtung ist noch nicht freigeschaltet.</p>
|
||||
<a href="tuxflotte:abmelden">Abmelden</a>
|
||||
</main></body></html>
|
||||
"""
|
||||
# --- Personalisierung (ADR-0041, Schritt 2) ----------------------------------
|
||||
#
|
||||
# Der Kunden-Admin erzeugt in der Geräte-Info einen Aktivierungscode, am Gerät
|
||||
# löst ihn das Formular der Kundenplattform ein (Kiosk im Konto
|
||||
# "Personalisierung"), die Bestätigung kommt per Mail-Link. Die Kundenplattform
|
||||
# hasht das Passwort, bevor es hierher kommt; Klartext erreicht anode nie.
|
||||
# Angelegt wird das Konto erst in Schritt 3 vom Agent.
|
||||
|
||||
PERSONALISIERUNG_CODE_ZEICHEN = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
|
||||
PERSONALISIERUNG_CODE_GUELTIG = timedelta(days=7)
|
||||
PERSONALISIERUNG_BESTAETIGUNG_FRIST = timedelta(hours=1)
|
||||
PERSONALISIERUNG_LAUFEND = ("code_offen", "eingereicht", "bestaetigt")
|
||||
|
||||
# Anmeldenamen: Kleinbuchstaben, Ziffern, Punkt, Binde- und Unterstrich,
|
||||
# beginnend mit einem Buchstaben. Systemkonten prüft die Rolle am Gerät
|
||||
# zusätzlich gegen /etc/passwd.
|
||||
PERSONALISIERUNG_ANMELDENAME_MUSTER = re.compile(r"^[a-z][a-z0-9._-]{1,30}$")
|
||||
PERSONALISIERUNG_RESERVIERT = {
|
||||
"root", "gast", "personalisierung", "tuxflotte", "admin", "administrator",
|
||||
"daemon", "bin", "sys", "sync", "games", "man", "lp", "mail", "news", "uucp",
|
||||
"proxy", "www-data", "backup", "list", "irc", "gnats", "nobody", "messagebus",
|
||||
"sshd", "lightdm", "gdm", "polkitd", "avahi", "colord", "pulse", "rtkit",
|
||||
"saned", "syslog", "dnsmasq", "usbmux", "kernoops", "geoclue", "cups-pk-helper",
|
||||
"speech-dispatcher", "hplip", "fwupd-refresh", "tss", "uuidd", "tcpdump",
|
||||
"veyon", "ansible", "sudo", "wheel", "users", "nogroup",
|
||||
}
|
||||
PERSONALISIERUNG_RESERVIERT_PRAEFIXE = ("systemd-", "tuxflotte", "_")
|
||||
|
||||
DOMAIN_MUSTER = re.compile(r"^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$")
|
||||
EMAIL_MUSTER = re.compile(r"^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]{1,64}@([A-Za-z0-9.-]{1,253})$")
|
||||
|
||||
|
||||
@app.get("/personalisierung", response_class=HTMLResponse)
|
||||
def personalisierung_seite():
|
||||
return HTMLResponse(PERSONALISIERUNG_SEITE, headers={"Cache-Control": "no-store"})
|
||||
def personalisierung_code_normalisieren(code: str) -> str:
|
||||
return re.sub(r"[\s-]", "", code or "").upper()
|
||||
|
||||
|
||||
def personalisierung_code_hash(code: str) -> str:
|
||||
return hashlib.sha256(personalisierung_code_normalisieren(code).encode()).hexdigest()
|
||||
|
||||
|
||||
def personalisierung_token_hash(token: str) -> str:
|
||||
return hashlib.sha256((token or "").encode()).hexdigest()
|
||||
|
||||
|
||||
def personalisierung_code_erzeugen() -> str:
|
||||
zeichen = "".join(secrets.choice(PERSONALISIERUNG_CODE_ZEICHEN) for _ in range(12))
|
||||
return f"{zeichen[0:4]}-{zeichen[4:8]}-{zeichen[8:12]}"
|
||||
|
||||
|
||||
def anmeldename_fehler(anmeldename: str) -> str | None:
|
||||
if not PERSONALISIERUNG_ANMELDENAME_MUSTER.match(anmeldename):
|
||||
return (
|
||||
"Der Anmeldename muss mit einem Kleinbuchstaben beginnen und darf nur "
|
||||
"Kleinbuchstaben, Ziffern, Punkt, Binde- und Unterstrich enthalten (2 bis 31 Zeichen)."
|
||||
)
|
||||
if anmeldename in PERSONALISIERUNG_RESERVIERT or anmeldename.startswith(PERSONALISIERUNG_RESERVIERT_PRAEFIXE):
|
||||
return "Dieser Anmeldename ist reserviert. Bitte einen anderen wählen."
|
||||
return None
|
||||
|
||||
|
||||
def email_domaenen_normalisieren(domaenen: list[str]) -> tuple[list[str], str | None]:
|
||||
ergebnis = []
|
||||
for eintrag in domaenen:
|
||||
domain = (eintrag or "").strip().lower().lstrip("@").rstrip(".")
|
||||
if not domain:
|
||||
continue
|
||||
if not DOMAIN_MUSTER.match(domain):
|
||||
return [], f"„{eintrag.strip()}“ ist keine gültige Domäne."
|
||||
if domain not in ergebnis:
|
||||
ergebnis.append(domain)
|
||||
return ergebnis, None
|
||||
|
||||
|
||||
def personalisierung_verfallene_markieren(cur) -> None:
|
||||
# Lazy statt Zeitgeber: jeder Zugriff räumt zuerst auf. Verfallene
|
||||
# Einreichungen verlieren Passwort-Hash und Adresse (nie bestätigt, also
|
||||
# womöglich ein Tippfehler oder eine fremde Adresse).
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'verfallen', passwort_hash = NULL, email = NULL, bestaetigung_token_hash = NULL
|
||||
WHERE (status = 'code_offen' AND code_gueltig_bis < now())
|
||||
OR (status = 'eingereicht' AND bestaetigung_bis < now())
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
PERSONALISIERUNG_SPALTEN = (
|
||||
"p.id, p.device_id, p.status, p.code_gueltig_bis, p.bestehenden_behalten, p.erstellt_von, "
|
||||
"p.erstellt_am, p.email, p.anmeldename, p.eingereicht_am, p.bestaetigung_bis, p.bestaetigt_am, "
|
||||
"p.angelegt_am, p.beendet_am"
|
||||
)
|
||||
|
||||
|
||||
def personalisierung_als_dict(row) -> dict:
|
||||
def zeit(wert):
|
||||
return wert.isoformat() if wert else None
|
||||
|
||||
return {
|
||||
"id": str(row[0]),
|
||||
"device_id": str(row[1]),
|
||||
"status": row[2],
|
||||
"code_gueltig_bis": zeit(row[3]),
|
||||
"bestehenden_behalten": row[4],
|
||||
"erstellt_von": row[5],
|
||||
"erstellt_am": zeit(row[6]),
|
||||
"email": row[7],
|
||||
"anmeldename": row[8],
|
||||
"eingereicht_am": zeit(row[9]),
|
||||
"bestaetigung_bis": zeit(row[10]),
|
||||
"bestaetigt_am": zeit(row[11]),
|
||||
"angelegt_am": zeit(row[12]),
|
||||
"beendet_am": zeit(row[13]),
|
||||
}
|
||||
|
||||
|
||||
def fetch_personalisierungen_geraet(device_id: str) -> list[dict]:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
f"SELECT {PERSONALISIERUNG_SPALTEN} FROM personalisierungen p "
|
||||
"WHERE p.device_id = %s ORDER BY p.erstellt_am DESC",
|
||||
(device_id,),
|
||||
)
|
||||
return [personalisierung_als_dict(row) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def fetch_personalisierungen_organisation(organization_id: str) -> dict[str, dict]:
|
||||
"""
|
||||
Für die Geräteliste: je Gerät der laufende Vorgang und die angelegten
|
||||
Konten, in einer Abfrage statt einer je Gerät.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
f"SELECT {PERSONALISIERUNG_SPALTEN} FROM personalisierungen p "
|
||||
"WHERE p.organization_id = %s AND p.status IN ('code_offen', 'eingereicht', 'bestaetigt', 'angelegt') "
|
||||
"ORDER BY p.erstellt_am",
|
||||
(organization_id,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
|
||||
ergebnis: dict[str, dict] = {}
|
||||
for row in rows:
|
||||
eintrag = personalisierung_als_dict(row)
|
||||
geraet = ergebnis.setdefault(eintrag["device_id"], {"laufend": None, "konten": []})
|
||||
if eintrag["status"] == "angelegt":
|
||||
geraet["konten"].append(eintrag)
|
||||
else:
|
||||
geraet["laufend"] = eintrag
|
||||
return ergebnis
|
||||
|
||||
|
||||
def create_personalisierung_code(device_id: str, bestehenden_behalten: bool, erstellt_von: str | None):
|
||||
code = personalisierung_code_erzeugen()
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("SELECT organization_id FROM devices WHERE id = %s", (device_id,))
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
# Ein neuer Code ersetzt einen laufenden Vorgang.
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'zurueckgezogen', passwort_hash = NULL, bestaetigung_token_hash = NULL
|
||||
WHERE device_id = %s AND status IN ('code_offen', 'eingereicht', 'bestaetigt')
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO personalisierungen
|
||||
(id, device_id, organization_id, status, code_hash, code_gueltig_bis,
|
||||
bestehenden_behalten, erstellt_von)
|
||||
VALUES (%s, %s, %s, 'code_offen', %s, %s, %s, %s)
|
||||
RETURNING code_gueltig_bis
|
||||
""",
|
||||
(
|
||||
uuid4(), device_id, row[0], personalisierung_code_hash(code),
|
||||
datetime.now(timezone.utc) + PERSONALISIERUNG_CODE_GUELTIG,
|
||||
bestehenden_behalten, erstellt_von,
|
||||
),
|
||||
)
|
||||
gueltig_bis = cur.fetchone()[0]
|
||||
|
||||
return {"code": code, "code_gueltig_bis": gueltig_bis.isoformat()}
|
||||
|
||||
|
||||
def withdraw_personalisierung(device_id: str) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'zurueckgezogen', passwort_hash = NULL, bestaetigung_token_hash = NULL
|
||||
WHERE device_id = %s AND status IN ('code_offen', 'eingereicht', 'bestaetigt')
|
||||
""",
|
||||
(device_id,),
|
||||
)
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
def submit_personalisierung(code: str, email: str, anmeldename: str, passwort_hash: str):
|
||||
"""
|
||||
Gibt (ergebnis, fehler) zurück. Unbekannte, verbrauchte und abgelaufene
|
||||
Codes ergeben dieselbe Meldung - sie verrät nicht, ob ein Code existiert.
|
||||
"""
|
||||
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT p.id, p.device_id, o.name, o.email_domaenen, d.hostname
|
||||
FROM personalisierungen p
|
||||
JOIN organizations o ON o.id = p.organization_id
|
||||
JOIN devices d ON d.id = p.device_id
|
||||
WHERE p.code_hash = %s AND p.status = 'code_offen'
|
||||
FOR UPDATE OF p
|
||||
""",
|
||||
(personalisierung_code_hash(code),),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return None, "Der Aktivierungscode ist ungültig, schon verwendet oder abgelaufen."
|
||||
|
||||
personalisierung_id, device_id, org_name, domaenen, hostname = row
|
||||
|
||||
treffer = EMAIL_MUSTER.match(email)
|
||||
if not treffer:
|
||||
return None, "Die E-Mail-Adresse ist ungültig."
|
||||
if domaenen and treffer.group(1).lower() not in domaenen:
|
||||
return None, "Bitte eine Adresse deiner Einrichtung verwenden (" + ", ".join("@" + d for d in domaenen) + ")."
|
||||
|
||||
fehler = anmeldename_fehler(anmeldename)
|
||||
if fehler:
|
||||
return None, fehler
|
||||
|
||||
cur.execute(
|
||||
"SELECT 1 FROM personalisierungen WHERE device_id = %s AND status = 'angelegt' AND anmeldename = %s",
|
||||
(device_id, anmeldename),
|
||||
)
|
||||
if cur.fetchone():
|
||||
return None, "Diesen Anmeldenamen gibt es auf diesem Gerät schon."
|
||||
|
||||
token = secrets.token_urlsafe(32)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'eingereicht', email = %s, anmeldename = %s, passwort_hash = %s,
|
||||
bestaetigung_token_hash = %s, eingereicht_am = now(), bestaetigung_bis = %s
|
||||
WHERE id = %s
|
||||
RETURNING bestaetigung_bis
|
||||
""",
|
||||
(
|
||||
email, anmeldename, passwort_hash, personalisierung_token_hash(token),
|
||||
datetime.now(timezone.utc) + PERSONALISIERUNG_BESTAETIGUNG_FRIST, personalisierung_id,
|
||||
),
|
||||
)
|
||||
bestaetigung_bis = cur.fetchone()[0]
|
||||
|
||||
return {
|
||||
"personalisierung_id": str(personalisierung_id),
|
||||
"token": token,
|
||||
"bestaetigung_bis": bestaetigung_bis.isoformat(),
|
||||
"organisation": org_name,
|
||||
"hostname": hostname,
|
||||
}, None
|
||||
|
||||
|
||||
def confirm_personalisierung(token: str):
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
personalisierung_verfallene_markieren(cur)
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE personalisierungen
|
||||
SET status = 'bestaetigt', bestaetigt_am = now(), bestaetigung_token_hash = NULL
|
||||
WHERE bestaetigung_token_hash = %s AND status = 'eingereicht'
|
||||
RETURNING anmeldename, (SELECT hostname FROM devices WHERE id = device_id)
|
||||
""",
|
||||
(personalisierung_token_hash(token),),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
|
||||
if row is None:
|
||||
return None
|
||||
return {"anmeldename": row[0], "hostname": row[1]}
|
||||
|
||||
|
||||
def set_email_domaenen(organization_id: str, domaenen: list[str]) -> bool:
|
||||
with get_database_connection() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("UPDATE organizations SET email_domaenen = %s WHERE id = %s", (domaenen, organization_id))
|
||||
return cur.rowcount > 0
|
||||
|
||||
|
||||
class CreatePersonalisierungRequest(BaseModel):
|
||||
bestehenden_behalten: bool = False
|
||||
erstellt_von: str | None = None
|
||||
|
||||
|
||||
class SubmitPersonalisierungRequest(BaseModel):
|
||||
code: str
|
||||
email: str
|
||||
anmeldename: str
|
||||
passwort_hash: str
|
||||
|
||||
|
||||
class ConfirmPersonalisierungRequest(BaseModel):
|
||||
token: str
|
||||
|
||||
|
||||
class SetEmailDomaenenRequest(BaseModel):
|
||||
domaenen: list[str]
|
||||
|
||||
|
||||
@app.get("/api/v1/devices/{device_id}/personalisierung")
|
||||
def get_personalisierung_geraet(device_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "personalisierungen": fetch_personalisierungen_geraet(device_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/devices/{device_id}/personalisierung")
|
||||
def post_personalisierung_code(
|
||||
device_id: str,
|
||||
payload: CreatePersonalisierungRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
ergebnis = create_personalisierung_code(device_id, payload.bestehenden_behalten, payload.erstellt_von)
|
||||
if ergebnis is None:
|
||||
return {"success": False, "error": "device_not_found", "message": "Das Gerät wurde nicht gefunden."}
|
||||
|
||||
return {"success": True, **ergebnis}
|
||||
|
||||
|
||||
@app.post("/api/v1/devices/{device_id}/personalisierung/zurueckziehen")
|
||||
def post_personalisierung_zurueckziehen(device_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": withdraw_personalisierung(device_id)}
|
||||
|
||||
|
||||
@app.get("/api/v1/organizations/{organization_id}/personalisierungen")
|
||||
def get_personalisierungen_organisation(organization_id: str, authorization: str | None = Header(default=None)):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
return {"success": True, "geraete": fetch_personalisierungen_organisation(organization_id)}
|
||||
|
||||
|
||||
@app.post("/api/v1/personalisierung/einreichen")
|
||||
def post_personalisierung_einreichen(
|
||||
payload: SubmitPersonalisierungRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
if not ROOT_PASSWORD_HASH_PATTERN.match(payload.passwort_hash):
|
||||
return {"success": False, "error": "invalid_request", "message": "Ungültiger Passwort-Hash."}
|
||||
|
||||
ergebnis, fehler = submit_personalisierung(
|
||||
payload.code, payload.email.strip(), payload.anmeldename.strip(), payload.passwort_hash
|
||||
)
|
||||
if fehler:
|
||||
return {"success": False, "error": "abgelehnt", "message": fehler}
|
||||
|
||||
return {"success": True, **ergebnis}
|
||||
|
||||
|
||||
@app.post("/api/v1/personalisierung/bestaetigen")
|
||||
def post_personalisierung_bestaetigen(
|
||||
payload: ConfirmPersonalisierungRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
ergebnis = confirm_personalisierung(payload.token)
|
||||
if ergebnis is None:
|
||||
return {"success": False, "error": "ungueltig", "message": "Der Link ist ungültig, schon verwendet oder abgelaufen."}
|
||||
|
||||
return {"success": True, **ergebnis}
|
||||
|
||||
|
||||
@app.put("/api/v1/organizations/{organization_id}/email-domaenen")
|
||||
def put_email_domaenen(
|
||||
organization_id: str,
|
||||
payload: SetEmailDomaenenRequest,
|
||||
authorization: str | None = Header(default=None),
|
||||
):
|
||||
if not require_service_token(authorization):
|
||||
return {"success": False, "error": "unauthorized", "message": "Fehlendes oder ungültiges Service-Token."}
|
||||
|
||||
domaenen, fehler = email_domaenen_normalisieren(payload.domaenen)
|
||||
if fehler:
|
||||
return {"success": False, "error": "invalid_request", "message": fehler}
|
||||
|
||||
if not set_email_domaenen(organization_id, domaenen):
|
||||
return {"success": False, "error": "organization_not_found", "message": "Die Organisation wurde nicht gefunden."}
|
||||
|
||||
return {"success": True, "domaenen": domaenen}
|
||||
|
||||
|
||||
@app.post("/api/v1/activate")
|
||||
|
||||
Reference in new issue
Block a user