mitai-jinkendo/backend/routers/features.py
Lars cbad50a987
Some checks failed
Build Test / lint-backend (push) Waiting to run
Build Test / build-frontend (push) Waiting to run
Deploy Development / deploy (push) Has been cancelled
fix: add missing feature check endpoint and features
Critical fixes for feature enforcement:
- Add GET /api/features/{feature_id}/check-access endpoint (was missing!)
- Add migration for missing features: data_export, csv_import
- These features were used in frontend but didn't exist in DB

This fixes:
- "No analysis available" when setting KI limit
- Export features not working
- Frontend calling non-existent API endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 12:57:29 +01:00

139 lines
4.4 KiB
Python

"""
Feature Management Endpoints for Mitai Jinkendo
Admin-only CRUD for features registry.
"""
from fastapi import APIRouter, HTTPException, Depends
from db import get_db, get_cursor, r2d
from auth import require_admin, require_auth, check_feature_access
router = APIRouter(prefix="/api/features", tags=["features"])
@router.get("")
def list_features(session: dict = Depends(require_admin)):
"""Admin: List all features."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("""
SELECT * FROM features
ORDER BY category, name
""")
return [r2d(r) for r in cur.fetchall()]
@router.post("")
def create_feature(data: dict, session: dict = Depends(require_admin)):
"""
Admin: Create new feature.
Required fields:
- id: Feature ID (e.g., 'new_data_source')
- name: Display name
- category: 'data', 'ai', 'export', 'integration'
- limit_type: 'count' or 'boolean'
- reset_period: 'never', 'daily', 'monthly'
- default_limit: INT or NULL (unlimited)
"""
feature_id = data.get('id', '').strip()
name = data.get('name', '').strip()
description = data.get('description', '')
category = data.get('category')
limit_type = data.get('limit_type', 'count')
reset_period = data.get('reset_period', 'never')
default_limit = data.get('default_limit')
if not feature_id or not name:
raise HTTPException(400, "ID und Name fehlen")
if category not in ['data', 'ai', 'export', 'integration']:
raise HTTPException(400, "Ungültige Kategorie")
if limit_type not in ['count', 'boolean']:
raise HTTPException(400, "limit_type muss 'count' oder 'boolean' sein")
if reset_period not in ['never', 'daily', 'monthly']:
raise HTTPException(400, "Ungültiger reset_period")
with get_db() as conn:
cur = get_cursor(conn)
# Check if ID already exists
cur.execute("SELECT id FROM features WHERE id = %s", (feature_id,))
if cur.fetchone():
raise HTTPException(400, f"Feature '{feature_id}' existiert bereits")
# Create feature
cur.execute("""
INSERT INTO features (
id, name, description, category, limit_type, reset_period, default_limit
)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (feature_id, name, description, category, limit_type, reset_period, default_limit))
conn.commit()
return {"ok": True, "id": feature_id}
@router.put("/{feature_id}")
def update_feature(feature_id: str, data: dict, session: dict = Depends(require_admin)):
"""Admin: Update feature."""
with get_db() as conn:
cur = get_cursor(conn)
updates = []
values = []
if 'name' in data:
updates.append('name = %s')
values.append(data['name'])
if 'description' in data:
updates.append('description = %s')
values.append(data['description'])
if 'default_limit' in data:
updates.append('default_limit = %s')
values.append(data['default_limit'])
if 'active' in data:
updates.append('active = %s')
values.append(data['active'])
if not updates:
return {"ok": True}
updates.append('updated = CURRENT_TIMESTAMP')
values.append(feature_id)
cur.execute(
f"UPDATE features SET {', '.join(updates)} WHERE id = %s",
values
)
conn.commit()
return {"ok": True}
@router.delete("/{feature_id}")
def delete_feature(feature_id: str, session: dict = Depends(require_admin)):
"""Admin: Delete feature (soft-delete: set active=false)."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("UPDATE features SET active = false WHERE id = %s", (feature_id,))
conn.commit()
return {"ok": True}
@router.get("/{feature_id}/check-access")
def check_access(feature_id: str, session: dict = Depends(require_auth)):
"""
User: Check if current user can access a feature.
Returns:
- allowed: bool - whether user can use the feature
- limit: int|null - total limit (null = unlimited)
- used: int - current usage
- remaining: int|null - remaining uses (null = unlimited)
- reason: str - why access is granted/denied
"""
profile_id = session['profile_id']
result = check_feature_access(profile_id, feature_id)
return result