diff --git a/backend/packages/app/src/windup_app/server/user/interface.py b/backend/packages/app/src/windup_app/server/user/interface.py index 84c22755..ec361a9a 100644 --- a/backend/packages/app/src/windup_app/server/user/interface.py +++ b/backend/packages/app/src/windup_app/server/user/interface.py @@ -46,9 +46,9 @@ def send_verification_code(self, email: str, purpose: str) -> None: @abstractmethod def login_by_code(self, input: LoginByCodeInput) -> LoginResult: - """邮箱+验证码登录,无账号时自动注册。 + """邮箱+验证码登录。内测期间不自动建号。 - :raises windup_common.exceptions.BizException: 验证码错误 / 已过期 / 账号已封禁。 + :raises windup_common.exceptions.BizException: 验证码错误 / 已过期 / 账号不存在 / 账号已封禁。 """ # -- 登出 ------------------------------------------------------------ diff --git a/backend/packages/app/src/windup_app/server/user/service.py b/backend/packages/app/src/windup_app/server/user/service.py index c80dbc27..87dd8551 100644 --- a/backend/packages/app/src/windup_app/server/user/service.py +++ b/backend/packages/app/src/windup_app/server/user/service.py @@ -280,6 +280,9 @@ def login_by_password_with_session( def send_verification_code(self, email: str, purpose: str) -> None: """发送邮箱验证码。""" + if purpose == "register": + raise BizException("内测期间暂不开放注册", code=BizCode.BAD_REQUEST) + # 频率限制 cooldown_key = VERIFY_COOLDOWN_KEY.format(email=email) if self.redis.get(cooldown_key): @@ -315,25 +318,17 @@ def login_by_code(self, input: LoginByCodeInput) -> LoginResult: def login_by_code_with_session( self, session: Session, input: LoginByCodeInput ) -> LoginResult: - """邮箱+验证码登录,无账号自动注册(带 session)。""" + """邮箱+验证码登录(带 session)。内测期间不自动建号。""" # 校验验证码 self._verify_code(input.email, input.code, "login") - # 查找或创建用户 user = session.scalar(select(User).where(User.email == input.email)) if user is None: - user = User(email=input.email, email_verified_at=datetime.now(timezone.utc)) - session.add(user) - session.flush() - # 自动注册送积分 - self._create_credit_account(session, user.id) - logger.info("[WINDUP] 验证码自动注册 | user_id=%s email=%s", user.id, user.email) - else: - if user.status == UserStatus.BANNED: - raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) - # 标记邮箱已验证 - if user.email_verified_at is None: - user.email_verified_at = datetime.now(timezone.utc) + raise BizException("账号不存在", code=BizCode.NOT_FOUND) + if user.status == UserStatus.BANNED: + raise BizException("账号已被封禁", code=BizCode.BAD_REQUEST) + if user.email_verified_at is None: + user.email_verified_at = datetime.now(timezone.utc) user.last_login_at = datetime.now(timezone.utc) session.flush() diff --git a/backend/packages/app/src/windup_app/web/api/auth.py b/backend/packages/app/src/windup_app/web/api/auth.py index b196a024..8570ac0c 100644 --- a/backend/packages/app/src/windup_app/web/api/auth.py +++ b/backend/packages/app/src/windup_app/web/api/auth.py @@ -111,19 +111,15 @@ class UserOut(BaseModel): @router.post("/register", response_model=Response[TokenResponse]) def register(body: RegisterRequest, session: Session = Depends(get_session)): - """邮箱+验证码+密码注册,注册即登录。""" - result = service.register_by_email_with_session( - session, - type("RegisterInput", (), {"email": body.email, "password": body.password, "code": body.code, "nickname": body.nickname})(), - ) - return Response.success( - TokenResponse( - access_token=result.access_token, - refresh_token=result.refresh_token, - user=result.user, - ), - message="注册成功", - ) + """邮箱+验证码+密码注册。 + + 内测期间关闭公开注册,路由与请求模型保留以便以后重新开放。 + """ + from windup_common.enums.biz_code import BizCode + from windup_common.exceptions import BizException + + del body, session + raise BizException("内测期间暂不开放注册", code=BizCode.BAD_REQUEST) @router.post("/login", response_model=Response[TokenResponse]) @@ -152,7 +148,7 @@ def send_code(body: SendCodeRequest): @router.post("/login-by-code", response_model=Response[TokenResponse]) def login_by_code(body: LoginByCodeRequest, session: Session = Depends(get_session)): - """验证码登录,无账号自动注册。""" + """验证码登录。内测期间不自动注册。""" result = service.login_by_code_with_session( session, type("LoginByCodeInput", (), {"email": body.email, "code": body.code})(), diff --git a/backend/tests/test_auth_registration_closed.py b/backend/tests/test_auth_registration_closed.py new file mode 100644 index 00000000..aa77c335 --- /dev/null +++ b/backend/tests/test_auth_registration_closed.py @@ -0,0 +1,30 @@ +"""内测关闭公开注册:公开建号路径必须被拒绝。""" + +from windup_common.enums.biz_code import BizCode + + +def test_register_endpoint_rejects_public_signup(client): + resp = client.post( + "/auth/register", + json={ + "email": "new@example.com", + "password": "password123", + "code": "123456", + }, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == BizCode.BAD_REQUEST + assert body["message"] == "内测期间暂不开放注册" + assert body["data"] is None + + +def test_send_code_rejects_register_purpose(client): + resp = client.post( + "/auth/send-code", + json={"email": "new@example.com", "purpose": "register"}, + ) + assert resp.status_code == 200 + body = resp.json() + assert body["code"] == BizCode.BAD_REQUEST + assert body["message"] == "内测期间暂不开放注册" diff --git a/backend/tests/test_user_service.py b/backend/tests/test_user_service.py index e8771584..184bd356 100644 --- a/backend/tests/test_user_service.py +++ b/backend/tests/test_user_service.py @@ -282,46 +282,64 @@ def test_login_banned_user(db_session, service, mock_email): # -- 验证码登录测试 ------------------------------------------------------ -def test_login_by_code_new_user(db_session, service, mock_email): - service._redis.get.return_value = "123456" +def test_login_by_code_unknown_email_does_not_create_user(db_session, service, mock_email): + """内测关闭公开注册后,验证码登录不得自动建号。""" + from sqlalchemy import select + service._redis.get.return_value = "123456" input_data = LoginByCodeInput(email="code@example.com", code="123456") - result = service.login_by_code_with_session(db_session, input_data) - assert result.user.email == "code@example.com" - assert result.user.email_verified_at is not None + with pytest.raises(BizException, match="账号不存在") as exc: + service.login_by_code_with_session(db_session, input_data) + from windup_common.enums.biz_code import BizCode -def test_login_by_code_new_user_creates_credit_account(db_session, service, mock_email): - """验证码自动注册时应创建积分账户并赠送初始积分。""" - from sqlalchemy import select - from windup_app.server.quota.model import CreditAccount, CreditTransaction - from windup_common.enums.quota import CreditReason - from windup_framework.config.quota import settings as quota_settings + assert exc.value.code == BizCode.NOT_FOUND + assert db_session.scalar(select(User).where(User.email == "code@example.com")) is None - service._redis.get.return_value = "123456" - input_data = LoginByCodeInput(email="auto@example.com", code="123456") - result = service.login_by_code_with_session(db_session, input_data) - user_id = result.user.id +def test_send_verification_code_rejects_register_purpose(service, mock_email): + service._redis.get.return_value = None - # 验证积分账户已创建 - account = db_session.scalar( - select(CreditAccount).where(CreditAccount.user_id == user_id) + with pytest.raises(BizException, match="内测期间暂不开放注册"): + service.send_verification_code("new@example.com", "register") + + mock_email.send_verification_code.assert_not_called() + + +def test_login_by_code_banned_user(db_session, service, mock_email): + from sqlalchemy import select + + service._redis.get.return_value = "123456" + service.register_by_email_with_session( + db_session, + RegisterInput(email="banned-code@example.com", password="pass123", code="123456"), ) - assert account is not None - assert account.balance == quota_settings.register_gift_amount - assert account.total_earned == quota_settings.register_gift_amount + user = db_session.scalar(select(User).where(User.email == "banned-code@example.com")) + user.status = UserStatus.BANNED + db_session.flush() - # 验证赠送流水已记录 - txn = db_session.scalar( - select(CreditTransaction).where( - CreditTransaction.user_id == user_id, - CreditTransaction.reason == CreditReason.REGISTER_GIFT, + service._redis.get.return_value = "654321" + with pytest.raises(BizException, match="账号已被封禁"): + service.login_by_code_with_session( + db_session, + LoginByCodeInput(email="banned-code@example.com", code="654321"), ) + + +def test_login_by_code_marks_unverified_email(db_session, service): + user = User(email="unverified@example.com", password_hash=_hash_password("pass123")) + db_session.add(user) + db_session.flush() + + service._redis.get.return_value = "123456" + result = service.login_by_code_with_session( + db_session, + LoginByCodeInput(email="unverified@example.com", code="123456"), ) - assert txn is not None - assert txn.delta == quota_settings.register_gift_amount + + assert result.user.email == "unverified@example.com" + assert result.user.email_verified_at is not None def test_login_by_code_existing_user(db_session, service, mock_email):