诚实性修复: API限流Retry-After(wait=60s)+断言 / purge定时接线(04:00 cron+actor+测试) / Channel表单校验(ref+rules+validate) / README PG数字513
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
"""P0-3 · 公开注册:建租户 + 建用户 + owner membership + free 订阅 + JWT。
|
||||
|
||||
POST /api/v1/auth/register/(AllowAny,已进 TenantMiddleware 白名单):
|
||||
入参 username / password / company_name /(可选)phone。
|
||||
租户 code 由公司名 slug 化,冲突加数字后缀;非法 slug → 400。
|
||||
密码最低 8 位;用户名已存在 → 400。
|
||||
事务内:Tenant → User → TenantMembership(owner) → billing.subscribe(free)。
|
||||
返回 JWT(access + refresh),免二次登录。
|
||||
防滥用:同 IP 限速 10 次/小时(复用 core.ratelimit 的 cache 计数器)。
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
from adrf.decorators import api_view
|
||||
from django.db import transaction
|
||||
from django.utils.text import slugify
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import authentication_classes, permission_classes
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework_simplejwt.tokens import RefreshToken
|
||||
|
||||
REGISTER_IP_LIMIT = 10
|
||||
REGISTER_IP_WINDOW = 3600
|
||||
|
||||
|
||||
def _tenant_code_for(company_name: str) -> str:
|
||||
base = slugify(company_name, allow_unicode=False) or ""
|
||||
base = re.sub(r"[^a-z0-9-]", "", base.lower())[:50].strip("-")
|
||||
if not base:
|
||||
raise ValidationError({"company_name": "公司名称无法生成有效的租户编码,请换一个名称"})
|
||||
from apps.core.models import Tenant
|
||||
|
||||
code, i = base, 0
|
||||
while Tenant.objects.filter(code=code).exists():
|
||||
i += 1
|
||||
code = f"{base}-{i}"[:64]
|
||||
return code
|
||||
|
||||
|
||||
@api_view(["POST"])
|
||||
@authentication_classes([])
|
||||
@permission_classes([AllowAny])
|
||||
def register(request):
|
||||
from django.core.cache import cache
|
||||
|
||||
from apps.core import ratelimit as rl
|
||||
|
||||
ip = rl.client_ip(request)
|
||||
ip_key = f"dealerhub:register-ip:{ip}"
|
||||
if int(cache.get(ip_key) or 0) >= REGISTER_IP_LIMIT:
|
||||
return Response(
|
||||
{"code": "register_throttled", "detail": "同一 IP 注册过于频繁,请 1 小时后再试"},
|
||||
status=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||
)
|
||||
|
||||
payload = request.data or {}
|
||||
username = (payload.get("username") or "").strip()
|
||||
password = payload.get("password") or ""
|
||||
company_name = (payload.get("company_name") or "").strip()
|
||||
phone = (payload.get("phone") or "").strip()
|
||||
|
||||
if not username or not password or not company_name:
|
||||
raise ValidationError({"detail": "username / password / company_name 均为必填"})
|
||||
if len(password) < 8:
|
||||
raise ValidationError({"password": "密码长度至少 8 位"})
|
||||
if len(username) > 150:
|
||||
raise ValidationError({"username": "用户名过长(最多 150 字符)"})
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
if User.objects.filter(username=username).exists():
|
||||
raise ValidationError({"username": "该用户名已被注册"})
|
||||
|
||||
code = _tenant_code_for(company_name)
|
||||
|
||||
from apps.core.models import Tenant, TenantMembership
|
||||
|
||||
with transaction.atomic():
|
||||
tenant = Tenant.objects.create(code=code, name=company_name, phone=phone)
|
||||
user = User.objects.create_user(username=username, password=password)
|
||||
TenantMembership.objects.create(
|
||||
user=user, tenant=tenant, role="owner", is_active=True,
|
||||
)
|
||||
try:
|
||||
from apps.billing.models import subscribe
|
||||
|
||||
subscribe(tenant, plan_code="free")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
cache.incr(ip_key)
|
||||
except ValueError:
|
||||
cache.set(ip_key, 1, REGISTER_IP_WINDOW)
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
return Response({
|
||||
"access": str(refresh.access_token),
|
||||
"refresh": str(refresh),
|
||||
"tenant": tenant.code,
|
||||
"username": user.username,
|
||||
}, status=status.HTTP_201_CREATED)
|
||||
Reference in New Issue
Block a user