Feature: Gasthund-Zugang für Sitter
- sitting_subscriptions Tabelle (dog_id, owner_id, sitter_id, valid_until) - POST/DELETE/GET /api/sitting-access — Zugang gewähren/widerrufen/auflisten - GET /api/dogs gibt Gasthunde zurück (is_guest=True, sitting_until, owner_name) - Diary POST erlaubt Sitter-Schreibzugang; PATCH/DELETE nur für Besitzer - Dog-Switcher: GAST-Badge bei fremden Hunden - Dog-Profil: Sitter-Zugang-Sektion (nur für Besitzer), Freund auswählen + Datum - Diary Detail-View: Bearbeiten-Button für Gasthunde ausgeblendet
This commit is contained in:
parent
eef787cc72
commit
289158b2cd
10 changed files with 327 additions and 18 deletions
|
|
@ -54,6 +54,29 @@ def _own_dog(dog_id: int, user_id: int, conn):
|
|||
return dog
|
||||
|
||||
|
||||
def _can_read_dog(dog_id: int, user_id: int, conn):
|
||||
"""Eigener Hund ODER geteilter Hund ODER aktiver Sitter-Zugang (Lesezugriff)."""
|
||||
dog = conn.execute(
|
||||
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user_id)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
dog = conn.execute(
|
||||
"""SELECT d.id FROM dogs d
|
||||
JOIN dog_shares ds ON ds.dog_id = d.id
|
||||
WHERE d.id=? AND ds.shared_with_id=? AND ds.accepted_at IS NOT NULL""",
|
||||
(dog_id, user_id)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
dog = conn.execute(
|
||||
"""SELECT 1 FROM sitting_subscriptions
|
||||
WHERE dog_id=? AND sitter_id=? AND valid_until >= date('now')""",
|
||||
(dog_id, user_id)
|
||||
).fetchone()
|
||||
if not dog:
|
||||
raise HTTPException(404, "Hund nicht gefunden.")
|
||||
return dog
|
||||
|
||||
|
||||
def _validate_dog_ids(dog_ids: list[int], primary: int, user_id: int, conn) -> list[int]:
|
||||
"""Stellt sicher dass alle IDs dem User gehören. Gibt die bereinigte Liste zurück."""
|
||||
all_ids = list({primary} | set(dog_ids))
|
||||
|
|
@ -123,7 +146,7 @@ async def list_diary(dog_id: int, limit: int = 20, offset: int = 0,
|
|||
q: Optional[str] = None, milestone: int = 0,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
_can_read_dog(dog_id, user["id"], conn)
|
||||
extra = "AND (d.is_milestone=1 OR d.typ='meilenstein')" if milestone else ""
|
||||
if q:
|
||||
pattern = f"%{q}%"
|
||||
|
|
@ -167,8 +190,24 @@ async def create_diary(dog_id: int, data: DiaryCreate,
|
|||
pass
|
||||
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
all_dogs = _validate_dog_ids(data.dog_ids or [], dog_id, user["id"], conn)
|
||||
# Erlaubnis: eigener Hund ODER aktiver Sitter-Zugang
|
||||
dog = conn.execute("SELECT user_id FROM dogs WHERE id=?", (dog_id,)).fetchone()
|
||||
is_owner = dog and dog["user_id"] == user["id"]
|
||||
is_sitter = conn.execute("""
|
||||
SELECT 1 FROM sitting_subscriptions
|
||||
WHERE dog_id=? AND sitter_id=? AND valid_until >= date('now')
|
||||
""", (dog_id, user["id"])).fetchone()
|
||||
if not is_owner and not is_sitter:
|
||||
# Fallback: shared dog check
|
||||
try:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
except HTTPException:
|
||||
raise HTTPException(403, "Kein Zugriff auf diesen Hund.")
|
||||
# Sitter darf nur den Gasthund als einzigen Hund eintragen
|
||||
if is_sitter and not is_owner:
|
||||
all_dogs = [dog_id]
|
||||
else:
|
||||
all_dogs = _validate_dog_ids(data.dog_ids or [], dog_id, user["id"], conn)
|
||||
|
||||
conn.execute(
|
||||
"""INSERT INTO diary
|
||||
|
|
@ -320,7 +359,7 @@ async def nearby_places(dog_id: int, lat: float, lon: float,
|
|||
@router.get("/{dog_id}/diary/{entry_id}")
|
||||
async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
_can_read_dog(dog_id, user["id"], conn)
|
||||
row = conn.execute(
|
||||
"""SELECT DISTINCT d.* FROM diary d
|
||||
LEFT JOIN diary_dogs dd ON dd.diary_id = d.id
|
||||
|
|
@ -339,7 +378,17 @@ async def get_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
|||
async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate,
|
||||
user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
# Nur Besitzer des Hundes darf bearbeiten, NICHT Sitter
|
||||
entry_owner = conn.execute(
|
||||
"SELECT d.user_id FROM diary dg JOIN dogs d ON d.id=dg.dog_id WHERE dg.id=?",
|
||||
(entry_id,)
|
||||
).fetchone()
|
||||
if not entry_owner or entry_owner["user_id"] != user["id"]:
|
||||
# Prüfen ob geteilter Hund (dog_shares)
|
||||
try:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
except HTTPException:
|
||||
raise HTTPException(403, "Nur der Besitzer darf Einträge bearbeiten.")
|
||||
|
||||
# Prüfen ob Eintrag diesem Hund gehört (direkt oder via diary_dogs)
|
||||
exists = conn.execute(
|
||||
|
|
@ -383,7 +432,16 @@ async def update_diary(dog_id: int, entry_id: int, data: DiaryUpdate,
|
|||
@router.delete("/{dog_id}/diary/{entry_id}", status_code=204)
|
||||
async def delete_diary(dog_id: int, entry_id: int, user=Depends(get_current_user)):
|
||||
with db() as conn:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
# Nur Besitzer des Hundes darf löschen, NICHT Sitter
|
||||
entry_owner = conn.execute(
|
||||
"SELECT d.user_id FROM diary dg JOIN dogs d ON d.id=dg.dog_id WHERE dg.id=?",
|
||||
(entry_id,)
|
||||
).fetchone()
|
||||
if not entry_owner or entry_owner["user_id"] != user["id"]:
|
||||
try:
|
||||
_own_dog(dog_id, user["id"], conn)
|
||||
except HTTPException:
|
||||
raise HTTPException(403, "Nur der Besitzer darf Einträge löschen.")
|
||||
conn.execute(
|
||||
"DELETE FROM diary WHERE id=? AND dog_id=?", (entry_id, dog_id)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,30 @@ async def list_dogs(user=Depends(get_current_user)):
|
|||
WHERE ds.shared_with_id = ? AND ds.accepted_at IS NOT NULL""",
|
||||
(user["id"],)
|
||||
).fetchall()
|
||||
return [dict(r) for r in own] + [dict(r) for r in shared]
|
||||
guest_rows = conn.execute("""
|
||||
SELECT d.*, ss.id AS sub_id, ss.valid_until AS sitting_until,
|
||||
u.name AS owner_name, NULL AS shared_by, NULL AS share_role
|
||||
FROM sitting_subscriptions ss
|
||||
JOIN dogs d ON d.id = ss.dog_id
|
||||
JOIN users u ON u.id = ss.owner_id
|
||||
WHERE ss.sitter_id = ?
|
||||
AND ss.valid_until >= date('now')
|
||||
""", (user["id"],)).fetchall()
|
||||
|
||||
result = []
|
||||
for r in own:
|
||||
d = dict(r)
|
||||
d["is_guest"] = False
|
||||
result.append(d)
|
||||
for r in shared:
|
||||
d = dict(r)
|
||||
d["is_guest"] = False
|
||||
result.append(d)
|
||||
for r in guest_rows:
|
||||
d = dict(r)
|
||||
d["is_guest"] = True
|
||||
result.append(d)
|
||||
return result
|
||||
|
||||
|
||||
@router.post("")
|
||||
|
|
|
|||
72
backend/routes/sitting_access.py
Normal file
72
backend/routes/sitting_access.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""BAN YARO — Gasthund-Zugang (Sitter-Subscriptions)"""
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from database import db
|
||||
from auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class AccessCreate(BaseModel):
|
||||
dog_id: int
|
||||
sitter_id: int
|
||||
valid_until: str # 'YYYY-MM-DD'
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
async def grant_access(data: AccessCreate, user=Depends(get_current_user)):
|
||||
"""Besitzer gewährt Sitter-Zugang zu seinem Hund."""
|
||||
with db() as conn:
|
||||
dog = conn.execute("SELECT id, user_id FROM dogs WHERE id=?", (data.dog_id,)).fetchone()
|
||||
if not dog or dog["user_id"] != user["id"]:
|
||||
raise HTTPException(403, "Nicht dein Hund.")
|
||||
conn.execute("""
|
||||
INSERT INTO sitting_subscriptions (dog_id, owner_id, sitter_id, valid_until)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(dog_id, sitter_id) DO UPDATE SET valid_until=excluded.valid_until
|
||||
""", (data.dog_id, user["id"], data.sitter_id, data.valid_until))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.delete("/{sub_id}")
|
||||
async def revoke_access(sub_id: int, user=Depends(get_current_user)):
|
||||
"""Besitzer widerruft Zugang (oder Sitter meldet sich selbst ab)."""
|
||||
with db() as conn:
|
||||
row = conn.execute("SELECT * FROM sitting_subscriptions WHERE id=?", (sub_id,)).fetchone()
|
||||
if not row:
|
||||
raise HTTPException(404, "Nicht gefunden.")
|
||||
if row["owner_id"] != user["id"] and row["sitter_id"] != user["id"]:
|
||||
raise HTTPException(403, "Kein Zugriff.")
|
||||
conn.execute("DELETE FROM sitting_subscriptions WHERE id=?", (sub_id,))
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/my")
|
||||
async def my_subscriptions(user=Depends(get_current_user)):
|
||||
"""Gibt alle aktiven Gasthunde zurück (als Sitter oder Besitzer)."""
|
||||
with db() as conn:
|
||||
rows = conn.execute("""
|
||||
SELECT ss.id, ss.dog_id, ss.valid_until,
|
||||
d.name AS dog_name, d.foto_url, d.rasse,
|
||||
d.foto_zoom, d.foto_offset_x, d.foto_offset_y,
|
||||
u.name AS owner_name
|
||||
FROM sitting_subscriptions ss
|
||||
JOIN dogs d ON d.id = ss.dog_id
|
||||
JOIN users u ON u.id = ss.owner_id
|
||||
WHERE ss.sitter_id = ?
|
||||
AND ss.valid_until >= date('now')
|
||||
""", (user["id"],)).fetchall()
|
||||
granted = conn.execute("""
|
||||
SELECT ss.id, ss.dog_id, ss.valid_until,
|
||||
d.name AS dog_name,
|
||||
u.name AS sitter_name
|
||||
FROM sitting_subscriptions ss
|
||||
JOIN dogs d ON d.id = ss.dog_id
|
||||
JOIN users u ON u.id = ss.sitter_id
|
||||
WHERE ss.owner_id = ? AND ss.valid_until >= date('now')
|
||||
""", (user["id"],)).fetchall()
|
||||
return {
|
||||
"as_sitter": [dict(r) for r in rows],
|
||||
"as_owner": [dict(r) for r in granted],
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue