Sprint 0: Backend, Docker, KI-Layer mit Free/Premium-Trennung
This commit is contained in:
parent
84f49fafcf
commit
00be2bbcd5
17 changed files with 1107 additions and 0 deletions
0
backend/routes/__init__.py
Normal file
0
backend/routes/__init__.py
Normal file
87
backend/routes/auth.py
Normal file
87
backend/routes/auth.py
Normal 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
3
backend/routes/diary.py
Normal 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
146
backend/routes/dogs.py
Normal 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
3
backend/routes/health.py
Normal 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
3
backend/routes/ki.py
Normal 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
3
backend/routes/poison.py
Normal 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
3
backend/routes/push.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
"""BAN YARO — push Routes (Stub, wird ausgebaut)"""
|
||||
from fastapi import APIRouter
|
||||
router = APIRouter()
|
||||
Loading…
Add table
Add a link
Reference in a new issue