Files
dealerhub/backend/apps/core/register.py
T

106 lines
3.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)