infrasynth-backend-kit/infrasynth/features/views.py
jcv-dev 551b42eab5 feat: production-hardening pass across the kit
Close the gaps between the documented contract (API-STANDARD, TENANCY,
ENTITLEMENTS) and the implementation, and remove committed build artifacts.

Security:
- verify + process inbound webhooks (HMAC/handler verify, size limit,
  timestamp tolerance, idempotency via InboundEvent.external_id)
- real 2FA login flow (pre-auth challenge; tokens only after verify/recovery)
- wire HybridPermission into security/audit views; add API-key rotate and
  users/<id>/permissions|roles endpoints
- tenant-scoped throttling on by default; webhook replay protection
- verify MercadoPago webhook signatures
- login brute-force guard, configurable password policy, real ALTCHA PoW

Correctness:
- apply verified billing webhooks idempotently (subscription/entitlement/
  invoice/PaymentTransaction); scheduled payment lifecycle jobs
- capture audit update diffs automatically; add audit retention purge
- working notification retries, per-channel rate limits, log retention
- pluggable virus scanner, upload-size limit, pipeline toggle
- feature rollout %/environment targeting; settings-driven registrations
- workflow guards (instance cap, route depth, self-assignment, clone on re-entry)
- wire every previously-dead INFRASYNTH_* setting; drop truly dead ones

Delivery:
- README + CHANGELOG; CI format check + coverage gate
- keep test media out of the tree; untrack .coverage, __pycache__,
  egg-info, docs/ and invoice artifacts
2026-09-24 10:41:21 -05:00

61 lines
2.5 KiB
Python

from rest_framework import mixins, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .models import FeatureFlag, FeatureFlagOverride
from .serializers import FeatureFlagOverrideSerializer, FeatureFlagSerializer
from .services import FeatureService
class FeatureFlagViewSet(viewsets.ModelViewSet):
queryset = FeatureFlag.objects.all()
serializer_class = FeatureFlagSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return FeatureFlag.objects.all()
@action(detail=False, methods=["get"], url_path="active")
def active_flags(self, request):
from django.conf import settings
from infrasynth.tenancy.context import get_current_tenant
fs = FeatureService()
tenant = get_current_tenant()
flags = fs.get_active_flags(user=request.user, tenant_id=tenant.pk if tenant is not None else None)
# A list (not a map) so camelCase key conversion never mangles slug identifiers.
data = {"flags": [{"slug": slug, "enabled": enabled} for slug, enabled in sorted(flags.items())]}
config = getattr(settings, "INFRASYNTH_FEATURES", {})
if config.get("EXPOSE_PERMISSIONS_IN_ACTIVE_ENDPOINT"):
from infrasynth.security.services import AuthorizationService
data["effective_permissions"] = sorted(AuthorizationService().get_effective_permissions(request.user))
if config.get("EXPOSE_ROLES_IN_ACTIVE_ENDPOINT"):
roles_qs = getattr(request.user, "roles", None)
data["roles"] = list(roles_qs.values_list("slug", flat=True)) if roles_qs is not None else []
return Response(data)
@action(detail=False, methods=["get"], url_path="check/(?P<slug>[^/]+)")
def check_flag(self, request, slug=None):
from infrasynth.tenancy.context import get_current_tenant
fs = FeatureService()
tenant = get_current_tenant()
enabled = fs.is_enabled(slug, user=request.user, tenant_id=tenant.pk if tenant is not None else None)
return Response({"slug": slug, "is_enabled": enabled})
class FeatureFlagOverrideViewSet(
mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
queryset = FeatureFlagOverride.objects.all()
serializer_class = FeatureFlagOverrideSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return FeatureFlagOverride.objects.select_related("flag", "user").all()