Five new admin routers:
1. routers/features.py
- GET/POST/PUT/DELETE /api/features
- Feature registry CRUD
- Allows adding new limitable features without schema changes
2. routers/tiers_mgmt.py
- GET/POST/PUT/DELETE /api/tiers
- Subscription tier management
- Price configuration, sort order
3. routers/tier_limits.py
- GET /api/tier-limits - Complete Tier x Feature matrix
- PUT /api/tier-limits - Update single limit
- PUT /api/tier-limits/batch - Batch update
- DELETE /api/tier-limits - Remove limit (fallback to default)
- Matrix editor backend
4. routers/user_restrictions.py
- GET/POST/PUT/DELETE /api/user-restrictions
- User-specific feature overrides
- Highest priority in access hierarchy
- Includes reason field for documentation
5. routers/access_grants.py
- GET /api/access-grants - List grants with filters
- POST /api/access-grants - Manual grant creation
- PUT /api/access-grants/{id} - Extend/pause grants
- DELETE /api/access-grants/{id} - Revoke access
- Activity logging
All endpoints require admin authentication.
Completes backend API for v9c Phase 2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
122 lines
3.8 KiB
Python
122 lines
3.8 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
|
|
|
|
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}
|