feat: Self-Service-Kontenverwaltung + Organisationsdaten unter /organisation
Neue Rolle ist_organisationsadmin (auth.py), bewusst getrennt von is_superuser: is_superuser bleibt exklusiv internes, organisationsuebergreifendes Tuxflotte-Personal mit Zugriff auf /admin/* - ist_organisationsadmin ist auf die eigene Organisation beschränkt und kann nie über Self-Service gesetzt werden (organization_id/is_superuser kommen bei POST /organisation/konten nie aus dem Formular, sondern immer aus der Session). /organisation zeigt jetzt Kontakt-/Stammdaten (bearbeitbar) und die Kolleg:innen-Liste der eigenen Organisation (für alle sichtbar, Verwaltung nur für Org-Admins/internes Personal via require_org_admin). Selbstlöschung des eigenen Kontos wird verweigert. fetch_benutzer_fuer_organisation() von admin_kunden.py nach auth.py verschoben (jetzt von beiden Routern importiert, kein Duplikat) - liefert zusätzlich id + ist_organisationsadmin. admin_kunden.py bekommt eine neue Organisationsdaten-Sektion (nutzt denselben PATCH-Endpoint) und die neue Checkbox beim Konto-Anlegen, um den ersten Org-Admin einer neuen Organisation zu bootstrappen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
36d0346172
commit
ec11de1ee9
65
auth.py
65
auth.py
@ -10,7 +10,7 @@ from fastapi_users import BaseUserManager, FastAPIUsers, UUIDIDMixin, schemas
|
|||||||
from fastapi_users.authentication import AuthenticationBackend, CookieTransport, JWTStrategy
|
from fastapi_users.authentication import AuthenticationBackend, CookieTransport, JWTStrategy
|
||||||
from fastapi_users.db import BaseUserDatabase
|
from fastapi_users.db import BaseUserDatabase
|
||||||
|
|
||||||
from db import get_async_database_connection
|
from db import get_async_database_connection, get_database_connection
|
||||||
from templating import templates
|
from templating import templates
|
||||||
|
|
||||||
|
|
||||||
@ -19,7 +19,10 @@ SESSION_SECRET = os.environ.get("KUNDENPLATTFORM_SESSION_SECRET", "")
|
|||||||
COOKIE_NAME = "kundenplattform_session"
|
COOKIE_NAME = "kundenplattform_session"
|
||||||
COOKIE_MAX_AGE = 14 * 24 * 60 * 60 # 14 Tage, wie zuvor Starlettes SessionMiddleware-Default
|
COOKIE_MAX_AGE = 14 * 24 * 60 * 60 # 14 Tage, wie zuvor Starlettes SessionMiddleware-Default
|
||||||
|
|
||||||
BENUTZER_SPALTEN = "id, organization_id, email, hashed_password, is_active, is_superuser, is_verified"
|
BENUTZER_SPALTEN = (
|
||||||
|
"id, organization_id, email, hashed_password, is_active, is_superuser, "
|
||||||
|
"ist_organisationsadmin, is_verified"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -30,16 +33,21 @@ class User:
|
|||||||
organization_id: uuid.UUID
|
organization_id: uuid.UUID
|
||||||
is_active: bool = True
|
is_active: bool = True
|
||||||
is_superuser: bool = False
|
is_superuser: bool = False
|
||||||
|
ist_organisationsadmin: bool = False
|
||||||
is_verified: bool = True
|
is_verified: bool = True
|
||||||
|
|
||||||
|
|
||||||
class UserCreate(schemas.BaseUserCreate):
|
class UserCreate(schemas.BaseUserCreate):
|
||||||
organization_id: uuid.UUID
|
organization_id: uuid.UUID
|
||||||
|
ist_organisationsadmin: Optional[bool] = False
|
||||||
is_verified: Optional[bool] = True
|
is_verified: Optional[bool] = True
|
||||||
|
|
||||||
|
|
||||||
def _row_to_user(row) -> User:
|
def _row_to_user(row) -> User:
|
||||||
user_id, organization_id, email, hashed_password, is_active, is_superuser, is_verified = row
|
(
|
||||||
|
user_id, organization_id, email, hashed_password, is_active,
|
||||||
|
is_superuser, ist_organisationsadmin, is_verified,
|
||||||
|
) = row
|
||||||
|
|
||||||
# psycopg liefert TEXT-Spalten hier teils als bytes zurück, nicht als str.
|
# psycopg liefert TEXT-Spalten hier teils als bytes zurück, nicht als str.
|
||||||
if isinstance(hashed_password, bytes):
|
if isinstance(hashed_password, bytes):
|
||||||
@ -54,6 +62,7 @@ def _row_to_user(row) -> User:
|
|||||||
hashed_password=hashed_password,
|
hashed_password=hashed_password,
|
||||||
is_active=is_active,
|
is_active=is_active,
|
||||||
is_superuser=is_superuser,
|
is_superuser=is_superuser,
|
||||||
|
ist_organisationsadmin=ist_organisationsadmin,
|
||||||
is_verified=is_verified,
|
is_verified=is_verified,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,8 +95,9 @@ class UserDatabase(BaseUserDatabase[User, uuid.UUID]):
|
|||||||
await cur.execute(
|
await cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO benutzer
|
INSERT INTO benutzer
|
||||||
(id, organization_id, email, hashed_password, is_active, is_superuser, is_verified)
|
(id, organization_id, email, hashed_password, is_active, is_superuser,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
ist_organisationsadmin, is_verified)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||||
""",
|
""",
|
||||||
(
|
(
|
||||||
user_id,
|
user_id,
|
||||||
@ -96,6 +106,7 @@ class UserDatabase(BaseUserDatabase[User, uuid.UUID]):
|
|||||||
create_dict["hashed_password"],
|
create_dict["hashed_password"],
|
||||||
create_dict.get("is_active", True),
|
create_dict.get("is_active", True),
|
||||||
create_dict.get("is_superuser", False),
|
create_dict.get("is_superuser", False),
|
||||||
|
create_dict.get("ist_organisationsadmin", False),
|
||||||
create_dict.get("is_verified", True),
|
create_dict.get("is_verified", True),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@ -168,6 +179,50 @@ async def require_admin(user: User = Depends(get_current_user)) -> User:
|
|||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def require_org_admin(user: User = Depends(get_current_user)) -> User:
|
||||||
|
"""
|
||||||
|
Für Self-Service-Kontenverwaltung (/organisation/konten): erlaubt
|
||||||
|
entweder is_superuser (internes Tuxflotte-Personal, soll nicht
|
||||||
|
ausgesperrt sein, falls Support nötig ist) oder ist_organisationsadmin
|
||||||
|
(die neue, auf die eigene Organisation beschränkte Rolle). Bewusst kein
|
||||||
|
Ersatz für require_admin - is_superuser bleibt exklusiv für /admin/*.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (user.is_superuser or user.ist_organisationsadmin):
|
||||||
|
raise HTTPException(status_code=403, detail="Kein Admin-Zugriff für diese Organisation.")
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_benutzer_fuer_organisation(organization_id: str) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Kontenliste einer Organisation - gemeinsam genutzt vom internen
|
||||||
|
Admin-Bereich (admin_kunden.py) und der Self-Service-Seite
|
||||||
|
(routers/organisation.py), um die SQL-Abfrage nicht zu duplizieren
|
||||||
|
(siehe ADR-0017 fetch_gruppen()-Reuse-Pattern).
|
||||||
|
"""
|
||||||
|
|
||||||
|
with get_database_connection() as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, email, is_superuser, ist_organisationsadmin, created_at
|
||||||
|
FROM benutzer WHERE organization_id = %s ORDER BY email
|
||||||
|
""",
|
||||||
|
(organization_id,),
|
||||||
|
)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"id": str(benutzer_id),
|
||||||
|
"email": email,
|
||||||
|
"is_superuser": is_superuser,
|
||||||
|
"ist_organisationsadmin": ist_organisationsadmin,
|
||||||
|
"created_at": created_at,
|
||||||
|
}
|
||||||
|
for benutzer_id, email, is_superuser, ist_organisationsadmin, created_at in cur.fetchall()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
11
migrations/0005_organisationsadmin_rolle.sql
Normal file
11
migrations/0005_organisationsadmin_rolle.sql
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- Organisationsgebundene Admin-Rolle fuer Self-Service-Kontenverwaltung
|
||||||
|
-- (/organisation/konten) - bewusst getrennt von is_superuser, das
|
||||||
|
-- weiterhin ausschliesslich organisationsuebergreifendes, internes
|
||||||
|
-- Tuxflotte-Personal mit Zugriff auf /admin/* bedeutet. Eine Kundin darf
|
||||||
|
-- sich selbst nie is_superuser setzen koennen, nur dieses neue,
|
||||||
|
-- auf die eigene Organisation beschraenkte Flag.
|
||||||
|
ALTER TABLE benutzer ADD COLUMN ist_organisationsadmin BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
@ -2,27 +2,13 @@ from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
|||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from anode_client import anode_request
|
from anode_client import anode_request
|
||||||
from auth import UserCreate, UserManager, get_user_manager, require_admin
|
from auth import UserCreate, UserManager, fetch_benutzer_fuer_organisation, get_user_manager, require_admin
|
||||||
from db import get_database_connection
|
|
||||||
from templating import templates
|
from templating import templates
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin/kunden")
|
router = APIRouter(prefix="/admin/kunden")
|
||||||
|
|
||||||
|
|
||||||
def fetch_benutzer_fuer_organisation(organization_id: str) -> list[dict]:
|
|
||||||
with get_database_connection() as conn:
|
|
||||||
with conn.cursor() as cur:
|
|
||||||
cur.execute(
|
|
||||||
"SELECT email, is_superuser, created_at FROM benutzer WHERE organization_id = %s ORDER BY email",
|
|
||||||
(organization_id,),
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
{"email": email, "is_superuser": is_superuser, "created_at": created_at}
|
|
||||||
for email, is_superuser, created_at in cur.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def index(request: Request, user: dict = Depends(require_admin)):
|
def index(request: Request, user: dict = Depends(require_admin)):
|
||||||
result = anode_request("GET", "/api/v1/organizations")
|
result = anode_request("GET", "/api/v1/organizations")
|
||||||
@ -80,6 +66,33 @@ def detail(request: Request, organization_id: str, user: dict = Depends(require_
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{organization_id}/organisationsdaten")
|
||||||
|
def organisationsdaten_speichern(
|
||||||
|
organization_id: str,
|
||||||
|
name: str = Form(...),
|
||||||
|
kontakt_name: str = Form(default=""),
|
||||||
|
adresse: str = Form(default=""),
|
||||||
|
telefon: str = Form(default=""),
|
||||||
|
kontakt_email: str = Form(default=""),
|
||||||
|
notizen: str = Form(default=""),
|
||||||
|
user: dict = Depends(require_admin),
|
||||||
|
):
|
||||||
|
anode_request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/v1/organizations/{organization_id}",
|
||||||
|
json={
|
||||||
|
"name": name,
|
||||||
|
"kontakt_name": kontakt_name or None,
|
||||||
|
"adresse": adresse or None,
|
||||||
|
"telefon": telefon or None,
|
||||||
|
"kontakt_email": kontakt_email or None,
|
||||||
|
"notizen": notizen or None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse(f"/admin/kunden/{organization_id}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/{organization_id}/gruppen")
|
@router.post("/{organization_id}/gruppen")
|
||||||
def gruppe_anlegen(organization_id: str, name: str = Form(...), user: dict = Depends(require_admin)):
|
def gruppe_anlegen(organization_id: str, name: str = Form(...), user: dict = Depends(require_admin)):
|
||||||
anode_request("POST", f"/api/v1/organizations/{organization_id}/gruppen", json={"name": name})
|
anode_request("POST", f"/api/v1/organizations/{organization_id}/gruppen", json={"name": name})
|
||||||
@ -215,6 +228,7 @@ async def konto_anlegen(
|
|||||||
email: str = Form(...),
|
email: str = Form(...),
|
||||||
password: str = Form(...),
|
password: str = Form(...),
|
||||||
is_superuser: str = Form(default=""),
|
is_superuser: str = Form(default=""),
|
||||||
|
ist_organisationsadmin: str = Form(default=""),
|
||||||
user: dict = Depends(require_admin),
|
user: dict = Depends(require_admin),
|
||||||
user_manager: UserManager = Depends(get_user_manager),
|
user_manager: UserManager = Depends(get_user_manager),
|
||||||
):
|
):
|
||||||
@ -224,6 +238,7 @@ async def konto_anlegen(
|
|||||||
password=password,
|
password=password,
|
||||||
organization_id=organization_id,
|
organization_id=organization_id,
|
||||||
is_superuser=is_superuser == "on",
|
is_superuser=is_superuser == "on",
|
||||||
|
ist_organisationsadmin=ist_organisationsadmin == "on",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,14 @@ from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
|||||||
from fastapi.responses import RedirectResponse
|
from fastapi.responses import RedirectResponse
|
||||||
|
|
||||||
from anode_client import anode_request
|
from anode_client import anode_request
|
||||||
from auth import get_current_user
|
from auth import (
|
||||||
|
UserCreate,
|
||||||
|
UserManager,
|
||||||
|
fetch_benutzer_fuer_organisation,
|
||||||
|
get_current_user,
|
||||||
|
get_user_manager,
|
||||||
|
require_org_admin,
|
||||||
|
)
|
||||||
from templating import templates
|
from templating import templates
|
||||||
|
|
||||||
|
|
||||||
@ -16,13 +23,85 @@ def organisation_ansicht(request: Request, user=Depends(get_current_user)):
|
|||||||
if not result.get("success"):
|
if not result.get("success"):
|
||||||
raise HTTPException(status_code=404, detail="Organisation wurde nicht gefunden.")
|
raise HTTPException(status_code=404, detail="Organisation wurde nicht gefunden.")
|
||||||
|
|
||||||
|
benutzer = fetch_benutzer_fuer_organisation(str(user.organization_id))
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "organisation.html", {"user": user, "organization": result["organization"]}
|
request,
|
||||||
|
"organisation.html",
|
||||||
|
{"user": user, "organization": result["organization"], "benutzer": benutzer},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/organisation")
|
@router.post("/organisation")
|
||||||
def organisation_speichern(name: str = Form(...), user=Depends(get_current_user)):
|
def organisation_speichern(
|
||||||
anode_request("PATCH", f"/api/v1/organizations/{user.organization_id}", json={"name": name})
|
name: str = Form(...),
|
||||||
|
kontakt_name: str = Form(default=""),
|
||||||
|
adresse: str = Form(default=""),
|
||||||
|
telefon: str = Form(default=""),
|
||||||
|
kontakt_email: str = Form(default=""),
|
||||||
|
notizen: str = Form(default=""),
|
||||||
|
user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
anode_request(
|
||||||
|
"PATCH",
|
||||||
|
f"/api/v1/organizations/{user.organization_id}",
|
||||||
|
json={
|
||||||
|
"name": name,
|
||||||
|
"kontakt_name": kontakt_name or None,
|
||||||
|
"adresse": adresse or None,
|
||||||
|
"telefon": telefon or None,
|
||||||
|
"kontakt_email": kontakt_email or None,
|
||||||
|
"notizen": notizen or None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse("/organisation", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/organisation/konten")
|
||||||
|
async def konto_anlegen(
|
||||||
|
email: str = Form(...),
|
||||||
|
password: str = Form(...),
|
||||||
|
ist_organisationsadmin: str = Form(default=""),
|
||||||
|
user=Depends(require_org_admin),
|
||||||
|
user_manager: UserManager = Depends(get_user_manager),
|
||||||
|
):
|
||||||
|
# organization_id kommt bewusst nie aus dem Formular, sondern immer aus
|
||||||
|
# der Session des anfragenden Nutzers - verhindert, dass jemand per
|
||||||
|
# manipuliertem Request ein Konto in einer fremden Organisation anlegt.
|
||||||
|
# is_superuser wird beim Self-Service-Aufruf nie gesetzt (Default False)
|
||||||
|
# - das bleibt exklusiv dem internen Admin-Bereich vorbehalten (siehe
|
||||||
|
# ADR zu is_superuser vs. ist_organisationsadmin).
|
||||||
|
await user_manager.create(
|
||||||
|
UserCreate(
|
||||||
|
email=email,
|
||||||
|
password=password,
|
||||||
|
organization_id=user.organization_id,
|
||||||
|
ist_organisationsadmin=ist_organisationsadmin == "on",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return RedirectResponse("/organisation", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/organisation/konten/{konto_id}/loeschen")
|
||||||
|
async def konto_loeschen(
|
||||||
|
konto_id: str,
|
||||||
|
user=Depends(require_org_admin),
|
||||||
|
user_manager: UserManager = Depends(get_user_manager),
|
||||||
|
):
|
||||||
|
if str(user.id) == konto_id:
|
||||||
|
raise HTTPException(status_code=400, detail="Das eigene Konto kann nicht gelöscht werden.")
|
||||||
|
|
||||||
|
ziel = next(
|
||||||
|
(b for b in fetch_benutzer_fuer_organisation(str(user.organization_id)) if b["id"] == konto_id),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ziel is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Konto wurde nicht gefunden.")
|
||||||
|
|
||||||
|
ziel_user = await user_manager.get_by_email(ziel["email"])
|
||||||
|
await user_manager.delete(ziel_user)
|
||||||
|
|
||||||
return RedirectResponse("/organisation", status_code=303)
|
return RedirectResponse("/organisation", status_code=303)
|
||||||
|
|||||||
@ -4,6 +4,31 @@
|
|||||||
<p><a href="/admin/kunden">← Kunden</a></p>
|
<p><a href="/admin/kunden">← Kunden</a></p>
|
||||||
<h2>{{ organization.name if organization else organization_id }}</h2>
|
<h2>{{ organization.name if organization else organization_id }}</h2>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Organisationsdaten</h3>
|
||||||
|
<form method="post" action="/admin/kunden/{{ organization_id }}/organisationsdaten">
|
||||||
|
<label for="org_name">Name</label>
|
||||||
|
<input type="text" id="org_name" name="name" value="{{ organization.name if organization else '' }}" required>
|
||||||
|
|
||||||
|
<label for="org_kontakt_name">Ansprechpartner:in</label>
|
||||||
|
<input type="text" id="org_kontakt_name" name="kontakt_name" value="{{ organization.kontakt_name or '' if organization else '' }}">
|
||||||
|
|
||||||
|
<label for="org_adresse">Adresse</label>
|
||||||
|
<input type="text" id="org_adresse" name="adresse" value="{{ organization.adresse or '' if organization else '' }}">
|
||||||
|
|
||||||
|
<label for="org_telefon">Telefon</label>
|
||||||
|
<input type="text" id="org_telefon" name="telefon" value="{{ organization.telefon or '' if organization else '' }}">
|
||||||
|
|
||||||
|
<label for="org_kontakt_email">Kontakt-E-Mail</label>
|
||||||
|
<input type="email" id="org_kontakt_email" name="kontakt_email" value="{{ organization.kontakt_email or '' if organization else '' }}">
|
||||||
|
|
||||||
|
<label for="org_notizen">Notizen</label>
|
||||||
|
<textarea id="org_notizen" name="notizen" rows="3">{{ organization.notizen or '' if organization else '' }}</textarea>
|
||||||
|
|
||||||
|
<button type="submit">Speichern</button>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h3>Aktivierungscodes</h3>
|
<h3>Aktivierungscodes</h3>
|
||||||
<figure>
|
<figure>
|
||||||
@ -90,12 +115,13 @@
|
|||||||
<h3>Kundenkonten</h3>
|
<h3>Kundenkonten</h3>
|
||||||
<figure>
|
<figure>
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>E-Mail</th><th>Admin</th><th>Angelegt</th></tr></thead>
|
<thead><tr><th>E-Mail</th><th>Tuxflotte-Admin</th><th>Org-Admin</th><th>Angelegt</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for b in benutzer %}
|
{% for b in benutzer %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ b.email }}</td>
|
<td>{{ b.email }}</td>
|
||||||
<td>{{ "ja" if b.is_superuser else "nein" }}</td>
|
<td>{{ "ja" if b.is_superuser else "nein" }}</td>
|
||||||
|
<td>{{ "ja" if b.ist_organisationsadmin else "nein" }}</td>
|
||||||
<td>{{ b.created_at }}</td>
|
<td>{{ b.created_at }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -111,7 +137,11 @@
|
|||||||
<input type="password" id="password" name="password" required>
|
<input type="password" id="password" name="password" required>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" name="is_superuser">
|
<input type="checkbox" name="is_superuser">
|
||||||
Admin-Zugriff (organisationsübergreifend)
|
Admin-Zugriff (organisationsübergreifend, internes Tuxflotte-Personal)
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="ist_organisationsadmin">
|
||||||
|
Admin dieser Organisation (darf eigene Kolleg:innen-Konten verwalten)
|
||||||
</label>
|
</label>
|
||||||
<button type="submit">Anlegen</button>
|
<button type="submit">Anlegen</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@ -6,6 +6,70 @@
|
|||||||
<form method="post" action="/organisation">
|
<form method="post" action="/organisation">
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input type="text" id="name" name="name" value="{{ organization.name }}" required>
|
<input type="text" id="name" name="name" value="{{ organization.name }}" required>
|
||||||
|
|
||||||
|
<label for="kontakt_name">Ansprechpartner:in</label>
|
||||||
|
<input type="text" id="kontakt_name" name="kontakt_name" value="{{ organization.kontakt_name or '' }}">
|
||||||
|
|
||||||
|
<label for="adresse">Adresse</label>
|
||||||
|
<input type="text" id="adresse" name="adresse" value="{{ organization.adresse or '' }}">
|
||||||
|
|
||||||
|
<label for="telefon">Telefon</label>
|
||||||
|
<input type="text" id="telefon" name="telefon" value="{{ organization.telefon or '' }}">
|
||||||
|
|
||||||
|
<label for="kontakt_email">Kontakt-E-Mail</label>
|
||||||
|
<input type="email" id="kontakt_email" name="kontakt_email" value="{{ organization.kontakt_email or '' }}">
|
||||||
|
|
||||||
|
<label for="notizen">Notizen</label>
|
||||||
|
<textarea id="notizen" name="notizen" rows="3">{{ organization.notizen or '' }}</textarea>
|
||||||
|
|
||||||
<button type="submit">Speichern</button>
|
<button type="submit">Speichern</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Kolleg:innen</h3>
|
||||||
|
{% if not benutzer %}
|
||||||
|
<p>Keine Konten gefunden.</p>
|
||||||
|
{% else %}
|
||||||
|
<figure>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>E-Mail</th><th>Org-Admin</th><th>Angelegt</th>{% if user.is_superuser or user.ist_organisationsadmin %}<th></th>{% endif %}</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in benutzer %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.email }}</td>
|
||||||
|
<td>{{ "ja" if b.ist_organisationsadmin else "nein" }}</td>
|
||||||
|
<td>{{ b.created_at }}</td>
|
||||||
|
{% if user.is_superuser or user.ist_organisationsadmin %}
|
||||||
|
<td>
|
||||||
|
{% if b.id != user.id|string %}
|
||||||
|
<form method="post" action="/organisation/konten/{{ b.id }}/loeschen">
|
||||||
|
<button type="submit" class="outline secondary">Löschen</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</figure>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if user.is_superuser or user.ist_organisationsadmin %}
|
||||||
|
<details>
|
||||||
|
<summary role="button" class="secondary outline">Neues Konto anlegen</summary>
|
||||||
|
<form method="post" action="/organisation/konten">
|
||||||
|
<label for="konto_email">E-Mail</label>
|
||||||
|
<input type="email" id="konto_email" name="email" required>
|
||||||
|
<label for="konto_password">Passwort</label>
|
||||||
|
<input type="password" id="konto_password" name="password" required>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="ist_organisationsadmin">
|
||||||
|
Admin dieser Organisation (darf Kolleg:innen-Konten verwalten)
|
||||||
|
</label>
|
||||||
|
<button type="submit">Anlegen</button>
|
||||||
|
</form>
|
||||||
|
</details>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user