feat: complete Phase 4 enforcement for all features (backend)
Alle 11 Features blockieren jetzt bei Limit-Überschreitung: Batch 1 (bereits erledigt): - weight_entries, circumference_entries, caliper_entries Batch 2: - activity_entries - nutrition_entries (CSV import) - photos Batch 3: - ai_calls (einzelne Analysen) - ai_pipeline (3-stufige Gesamtanalyse) - data_export (CSV, JSON, ZIP) - data_import (ZIP) Entfernt: Alte check_ai_limit() Calls (ersetzt durch neue Feature-Limits) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d13c2c7e25
commit
4b8e6755dc
|
|
@ -37,15 +37,20 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
|
|||
"""Create new activity entry."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'activity_entries')
|
||||
log_feature_usage(pid, 'activity_entries', access, 'create')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"activity_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Aktivitätseinträge überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
eid = str(uuid.uuid4())
|
||||
d = e.model_dump()
|
||||
|
|
|
|||
|
|
@ -33,24 +33,20 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
|
|||
"""Export all data as CSV."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'data_export')
|
||||
log_feature_usage(pid, 'data_export', access, 'export_csv')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
# NOTE: Phase 2 does NOT block - just logs!
|
||||
|
||||
# Old permission check (keep for now)
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,))
|
||||
prof = cur.fetchone()
|
||||
if not prof or not prof['export_enabled']:
|
||||
raise HTTPException(403, "Export ist für dieses Profil deaktiviert")
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
# Build CSV
|
||||
output = io.StringIO()
|
||||
|
|
@ -104,23 +100,20 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict=
|
|||
"""Export all data as JSON."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'data_export')
|
||||
log_feature_usage(pid, 'data_export', access, 'export_json')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
|
||||
# Old permission check (keep for now)
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,))
|
||||
prof = cur.fetchone()
|
||||
if not prof or not prof['export_enabled']:
|
||||
raise HTTPException(403, "Export ist für dieses Profil deaktiviert")
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
# Collect all data
|
||||
data = {}
|
||||
|
|
@ -170,23 +163,26 @@ def export_zip(x_profile_id: Optional[str]=Header(default=None), session: dict=D
|
|||
"""Export all data as ZIP (CSV + JSON + photos) per specification."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'data_export')
|
||||
log_feature_usage(pid, 'data_export', access, 'export_zip')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
# Old permission check & get profile
|
||||
# Get profile
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
|
||||
prof = r2d(cur.fetchone())
|
||||
if not prof or not prof.get('export_enabled'):
|
||||
raise HTTPException(403, "Export ist für dieses Profil deaktiviert")
|
||||
|
||||
# Helper: CSV writer with UTF-8 BOM + semicolon
|
||||
def write_csv(zf, filename, rows, columns):
|
||||
|
|
|
|||
|
|
@ -44,16 +44,20 @@ async def import_zip(
|
|||
"""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'data_import')
|
||||
log_feature_usage(pid, 'data_import', access, 'import_zip')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"data_import {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
# NOTE: Phase 2 does NOT block - just logs!
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Importe überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
# Read uploaded file
|
||||
content = await file.read()
|
||||
|
|
|
|||
|
|
@ -255,19 +255,20 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa
|
|||
"""Run AI analysis with specified prompt template."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'ai_calls')
|
||||
log_feature_usage(pid, 'ai_calls', access, 'analyze')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"ai_calls {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
# NOTE: Phase 2 does NOT block - just logs!
|
||||
|
||||
# Old check (keep for now, but will be replaced in Phase 4)
|
||||
check_ai_limit(pid)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
# Get prompt template
|
||||
with get_db() as conn:
|
||||
|
|
@ -330,16 +331,19 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
|
|||
"""Run 3-stage pipeline analysis."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check pipeline feature access (boolean - enabled/disabled)
|
||||
# Phase 4: Check pipeline feature access (boolean - enabled/disabled)
|
||||
access_pipeline = check_feature_access(pid, 'ai_pipeline')
|
||||
log_feature_usage(pid, 'ai_pipeline', access_pipeline, 'pipeline')
|
||||
|
||||
if not access_pipeline['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"ai_pipeline {access_pipeline['reason']}"
|
||||
)
|
||||
# NOTE: Phase 2 does NOT block - just logs!
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Pipeline-Analyse ist nicht verfügbar. Bitte kontaktiere den Admin."
|
||||
)
|
||||
|
||||
# Also check ai_calls (pipeline uses API calls too)
|
||||
access_calls = check_feature_access(pid, 'ai_calls')
|
||||
|
|
@ -347,12 +351,14 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
|
|||
|
||||
if not access_calls['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"ai_calls {access_calls['reason']} (used: {access_calls['used']}, limit: {access_calls['limit']})"
|
||||
)
|
||||
|
||||
# Old check (keep for now)
|
||||
check_ai_limit(pid)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ({access_calls['used']}/{access_calls['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
data = _get_profile_data(pid)
|
||||
vars = _prepare_template_vars(data)
|
||||
|
|
|
|||
|
|
@ -34,16 +34,21 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona
|
|||
"""Import FDDB nutrition CSV."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
# Note: CSV import can create many entries - we check once before import
|
||||
access = check_feature_access(pid, 'nutrition_entries')
|
||||
log_feature_usage(pid, 'nutrition_entries', access, 'import_csv')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
raw = await file.read()
|
||||
try: text = raw.decode('utf-8')
|
||||
|
|
|
|||
|
|
@ -31,15 +31,20 @@ async def upload_photo(file: UploadFile=File(...), date: str="",
|
|||
"""Upload progress photo."""
|
||||
pid = get_pid(x_profile_id)
|
||||
|
||||
# Phase 2: Check feature access (non-blocking, log only)
|
||||
# Phase 4: Check feature access and ENFORCE
|
||||
access = check_feature_access(pid, 'photos')
|
||||
log_feature_usage(pid, 'photos', access, 'upload')
|
||||
|
||||
if not access['allowed']:
|
||||
logger.warning(
|
||||
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
||||
f"photos {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Limit erreicht: Du hast das Kontingent für Fotos überschritten ({access['used']}/{access['limit']}). "
|
||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
||||
)
|
||||
|
||||
fid = str(uuid.uuid4())
|
||||
ext = Path(file.filename).suffix or '.jpg'
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user