diff --git a/README.md b/README.md index a347fda..7d1f845 100644 --- a/README.md +++ b/README.md @@ -162,9 +162,23 @@ Gruppen sind über die API abrufbar: - `GET /api/groups/{id}/members` — Nur Members einer Gruppe - `GET /api/contacts/{id}` — Enthält `groups`-Feld mit Gruppennamen -Wird ein Kontakt gelöscht, wird die Mitgliedschaft in Gruppen -automatisch entfernt (`ON DELETE CASCADE`). Die Gruppe selbst bleibt -erhalten. +Wird eine Gruppe gelöscht, werden zugehörige Memberschaften automatisch +entfernt (`ON DELETE CASCADE`). Gelöschte Mitglied-Kontakte verbleiben +als Member-Eintrag in der Gruppe (ohne aufgelöste Kontaktdaten). Beim +nächsten vollen Re-Sync werden tote Memberschaften bereinigt. + +### Migration bei erstem Deploy + +Bei Bestands-DBs lagen Gruppen bisher als normale Kontakte in der +`contacts`-Tabelle. Nach dem Deploy müssen diese einmalig bereinigt +werden: + +1. Sync-Container stoppen: `docker compose stop icloud-contacts-sync` +2. Sync-State zurücksetzen: `DELETE FROM sync_state;` (erzwingt vollen Re-Sync) +3. Container neu starten: `docker compose start icloud-contacts-sync` + +Beim nächsten Sync-Lauf werden alle vCards neu klassifiziziert — +Gruppen landen in `groups`, Kontakte bleiben in `contacts`. ## 11. Web-Ansicht und API (interner Zugriff über Authelia) diff --git a/sql/schema.sql b/sql/schema.sql index b92e134..2a1ffac 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -77,7 +77,8 @@ CREATE TABLE IF NOT EXISTS group_members ( group_id INT NOT NULL, member_uid VARCHAR(255) NOT NULL, FOREIGN KEY (group_id) REFERENCES `groups`(id) ON DELETE CASCADE, - UNIQUE KEY uq_group_member (group_id, member_uid) + UNIQUE KEY uq_group_member (group_id, member_uid), + KEY idx_group_members_member_uid (member_uid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- Protokoll der Geburtstags-Mails, verhindert Doppelversand am selben Tag. diff --git a/src/api/main.py b/src/api/main.py index a0c6d25..83d201f 100644 --- a/src/api/main.py +++ b/src/api/main.py @@ -215,12 +215,11 @@ def list_sync_runs(current_user: str = Depends(get_current_user)): @app.get("/api/groups", response_model=GroupListResponse) def list_groups( - request: Request, limit: int = Query(default=50, le=200), offset: int = Query(default=0, ge=0), current_user: str = Depends(get_current_user), ): - account_name, is_admin = resolve_account_for_user(current_user) + account_name, _ = resolve_account_for_user(current_user) with db.get_connection() as conn: where_clause, params = _account_filter_clause(account_name) @@ -245,7 +244,7 @@ def list_groups( @app.get("/api/groups/{group_id}", response_model=GroupDetailOut) def get_group(group_id: int, current_user: str = Depends(get_current_user)): - account_name, is_admin = resolve_account_for_user(current_user) + account_name, _ = resolve_account_for_user(current_user) with db.get_connection() as conn: where_clause, params = _account_filter_clause(account_name) @@ -265,9 +264,9 @@ def get_group(group_id: int, current_user: str = Depends(get_current_user)): cur.execute( """SELECT gm.member_uid, c.id, c.full_name, c.given_name, c.family_name FROM group_members gm + JOIN `groups` g ON g.id = gm.group_id LEFT JOIN contacts c ON c.account = g.account AND c.uid = gm.member_uid - CROSS JOIN `groups` g - WHERE g.id = %s AND gm.group_id = g.id""", + WHERE g.id = %s""", (group_id,), ) members = cur.fetchall() @@ -286,24 +285,26 @@ def get_group(group_id: int, current_user: str = Depends(get_current_user)): @app.get("/api/groups/{group_id}/members") def get_group_members(group_id: int, current_user: str = Depends(get_current_user)): - account_name, is_admin = resolve_account_for_user(current_user) + account_name, _ = resolve_account_for_user(current_user) with db.get_connection() as conn: where_clause, params = _account_filter_clause(account_name) with conn.cursor() as cur: cur.execute( - f"""SELECT g.id FROM `groups` g {where_clause} + f"""SELECT g.id, g.account FROM `groups` g {where_clause} {"AND" if where_clause else "WHERE"} g.id = %s""", params + [group_id], ) - if not cur.fetchone(): + group = cur.fetchone() + if not group: return {} cur.execute( """SELECT gm.member_uid, c.id, c.full_name, c.given_name, c.family_name, c.organization, c.birthday, c.photo_url FROM group_members gm - LEFT JOIN contacts c ON c.uid = gm.member_uid + JOIN `groups` g ON g.id = gm.group_id + LEFT JOIN contacts c ON c.account = g.account AND c.uid = gm.member_uid WHERE gm.group_id = %s""", (group_id,), ) diff --git a/src/vcard_parser.py b/src/vcard_parser.py index 03fe680..7842aad 100644 --- a/src/vcard_parser.py +++ b/src/vcard_parser.py @@ -61,10 +61,6 @@ def parse_group(raw_text: str, account: str, etag: str | None = None) -> dict | else: member_uids.append(value) - if not member_uids: - logger.debug("Gruppe %s hat keine Members, überspringe", uid) - return None - return { "account": account, "uid": uid,