Sprint 0: Backend, Docker, KI-Layer mit Free/Premium-Trennung

This commit is contained in:
rene 2026-04-12 16:39:34 +02:00
parent 84f49fafcf
commit 00be2bbcd5
17 changed files with 1107 additions and 0 deletions

View file

87
backend/routes/auth.py Normal file
View file

@ -0,0 +1,87 @@
"""BAN YARO — Auth Routes"""
from fastapi import APIRouter, HTTPException, Response, Depends
from pydantic import BaseModel, EmailStr
from database import db
from auth import (
hash_password, verify_password, create_token,
get_current_user
)
router = APIRouter()
COOKIE_NAME = "by_token"
class LoginRequest(BaseModel):
email: EmailStr
password: str
class RegisterRequest(BaseModel):
email: EmailStr
password: str
name: str
def _set_cookie(response: Response, token: str):
response.set_cookie(
key=COOKIE_NAME, value=token,
httponly=True, secure=True, samesite="lax",
max_age=30 * 24 * 3600
)
@router.post("/register")
async def register(data: RegisterRequest, response: Response):
with db() as conn:
if conn.execute("SELECT 1 FROM users WHERE email=?", (data.email,)).fetchone():
raise HTTPException(400, "E-Mail bereits registriert.")
conn.execute(
"INSERT INTO users (email, pw_hash, name) VALUES (?,?,?)",
(data.email, hash_password(data.password), data.name)
)
user = conn.execute(
"SELECT id, rolle FROM users WHERE email=?", (data.email,)
).fetchone()
token = create_token(user["id"], user["rolle"])
_set_cookie(response, token)
return {"token": token, "name": data.name}
@router.post("/login")
async def login(data: LoginRequest, response: Response):
with db() as conn:
user = conn.execute(
"SELECT id, pw_hash, name, rolle, is_premium FROM users WHERE email=?",
(data.email,)
).fetchone()
if not user or not verify_password(data.password, user["pw_hash"]):
raise HTTPException(401, "E-Mail oder Passwort falsch.")
token = create_token(user["id"], user["rolle"])
_set_cookie(response, token)
with db() as conn:
conn.execute(
"UPDATE users SET last_login=datetime('now') WHERE id=?", (user["id"],)
)
return {"token": token, "name": user["name"], "is_premium": bool(user["is_premium"])}
@router.post("/logout")
async def logout(response: Response):
response.delete_cookie(COOKIE_NAME)
return {"ok": True}
@router.get("/me")
async def me(user=Depends(get_current_user)):
return {
"id": user["id"],
"name": user["name"],
"email": user["email"],
"rolle": user["rolle"],
"is_premium": bool(user["is_premium"]),
}

3
backend/routes/diary.py Normal file
View file

@ -0,0 +1,3 @@
"""BAN YARO — Tagebuch Routes (Stub, wird in Sprint 1 ausgebaut)"""
from fastapi import APIRouter
router = APIRouter()

146
backend/routes/dogs.py Normal file
View file

@ -0,0 +1,146 @@
"""BAN YARO — Hunde-Profil Routes"""
import os
import uuid
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
router = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
class DogCreate(BaseModel):
name: str
rasse: Optional[str] = None
geburtstag: Optional[str] = None
geschlecht: Optional[str] = None
gewicht_kg: Optional[float] = None
chip_nr: Optional[str] = None
bio: Optional[str] = None
is_public: bool = False
class DogUpdate(BaseModel):
name: Optional[str] = None
rasse: Optional[str] = None
geburtstag: Optional[str] = None
geschlecht: Optional[str] = None
gewicht_kg: Optional[float] = None
chip_nr: Optional[str] = None
bio: Optional[str] = None
is_public: Optional[bool] = None
@router.get("")
async def list_dogs(user=Depends(get_current_user)):
with db() as conn:
rows = conn.execute(
"SELECT * FROM dogs WHERE user_id=? ORDER BY id", (user["id"],)
).fetchall()
return [dict(r) for r in rows]
@router.post("")
async def create_dog(data: DogCreate, user=Depends(get_current_user)):
with db() as conn:
conn.execute(
"""INSERT INTO dogs (user_id, name, rasse, geburtstag, geschlecht,
gewicht_kg, chip_nr, bio, is_public)
VALUES (?,?,?,?,?,?,?,?,?)""",
(user["id"], data.name, data.rasse, data.geburtstag,
data.geschlecht, data.gewicht_kg, data.chip_nr,
data.bio, int(data.is_public))
)
dog = conn.execute(
"SELECT * FROM dogs WHERE user_id=? ORDER BY id DESC LIMIT 1",
(user["id"],)
).fetchone()
return dict(dog)
@router.get("/{dog_id}")
async def get_dog(dog_id: int, user=Depends(get_current_user)):
with db() as conn:
dog = conn.execute(
"SELECT * FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
).fetchone()
if not dog:
raise HTTPException(404, "Hund nicht gefunden.")
return dict(dog)
@router.patch("/{dog_id}")
async def update_dog(dog_id: int, data: DogUpdate, user=Depends(get_current_user)):
fields = {k: v for k, v in data.model_dump().items() if v is not None}
if not fields:
raise HTTPException(400, "Keine Änderungen angegeben.")
set_clause = ", ".join(f"{k}=?" for k in fields)
values = list(fields.values()) + [dog_id, user["id"]]
with db() as conn:
conn.execute(
f"UPDATE dogs SET {set_clause} WHERE id=? AND user_id=?", values
)
dog = conn.execute(
"SELECT * FROM dogs WHERE id=?", (dog_id,)
).fetchone()
return dict(dog)
@router.delete("/{dog_id}", status_code=204)
async def delete_dog(dog_id: int, user=Depends(get_current_user)):
with db() as conn:
conn.execute(
"DELETE FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
)
@router.post("/{dog_id}/photo")
async def upload_photo(
dog_id: int,
file: UploadFile = File(...),
user=Depends(get_current_user)
):
# Hund gehört dem User?
with db() as conn:
dog = conn.execute(
"SELECT id FROM dogs WHERE id=? AND user_id=?", (dog_id, user["id"])
).fetchone()
if not dog:
raise HTTPException(404, "Hund nicht gefunden.")
# Datei speichern
ext = os.path.splitext(file.filename or "")[1] or ".jpg"
filename = f"dog_{dog_id}_{uuid.uuid4().hex[:8]}{ext}"
path = os.path.join(MEDIA_DIR, "dogs", filename)
os.makedirs(os.path.dirname(path), exist_ok=True)
content = await file.read()
with open(path, "wb") as f:
f.write(content)
foto_url = f"/media/dogs/{filename}"
with db() as conn:
conn.execute("UPDATE dogs SET foto_url=? WHERE id=?", (foto_url, dog_id))
return {"foto_url": foto_url}
# Öffentliches Profil (für NFC-Tag, kein Login nötig)
@router.get("/public/{dog_id}")
async def public_dog_profile(dog_id: int):
with db() as conn:
dog = conn.execute(
"""SELECT d.id, d.name, d.rasse, d.geburtstag, d.foto_url, d.bio,
u.name as besitzer_name
FROM dogs d JOIN users u ON d.user_id=u.id
WHERE d.id=? AND d.is_public=1""",
(dog_id,)
).fetchone()
if not dog:
raise HTTPException(404, "Profil nicht gefunden oder nicht öffentlich.")
return dict(dog)

3
backend/routes/health.py Normal file
View file

@ -0,0 +1,3 @@
"""BAN YARO — health Routes (Stub, wird ausgebaut)"""
from fastapi import APIRouter
router = APIRouter()

3
backend/routes/ki.py Normal file
View file

@ -0,0 +1,3 @@
"""BAN YARO — ki Routes (Stub, wird ausgebaut)"""
from fastapi import APIRouter
router = APIRouter()

3
backend/routes/poison.py Normal file
View file

@ -0,0 +1,3 @@
"""BAN YARO — poison Routes (Stub, wird ausgebaut)"""
from fastapi import APIRouter
router = APIRouter()

3
backend/routes/push.py Normal file
View file

@ -0,0 +1,3 @@
"""BAN YARO — push Routes (Stub, wird ausgebaut)"""
from fastapi import APIRouter
router = APIRouter()