feat: create accounts with client IP (#40875)

This commit is contained in:
林玮 (Jade Lin)
2026-08-17 09:29:57 +00:00
committed by GitHub
parent 4249a0d052
commit 5ef8becd25
10 changed files with 106 additions and 16 deletions
@@ -184,13 +184,15 @@ class EmailRegisterResetApi(Resource):
if account:
raise EmailAlreadyInUseError()
ip_address = extract_remote_ip(request)
account = self._create_new_account(
email=normalized_email,
password=req_data.password_confirm,
timezone=req_data.timezone,
language=req_data.language,
ip_address=ip_address,
)
token_pair = AccountService.login(account=account, session=db.session(), ip_address=extract_remote_ip(request))
token_pair = AccountService.login(account=account, session=db.session(), ip_address=ip_address)
AccountService.reset_login_error_rate_limit(normalized_email)
return {"result": "success", "data": token_pair.model_dump()}
@@ -201,6 +203,7 @@ class EmailRegisterResetApi(Resource):
password: str,
timezone: str | None = None,
language: str | None = None,
ip_address: str | None = None,
) -> Account:
try:
return AccountService.create_account_and_tenant(
@@ -209,6 +212,7 @@ class EmailRegisterResetApi(Resource):
password=password,
interface_language=get_valid_language(language),
timezone=timezone,
ip_address=ip_address,
session=db.session(),
)
except SeatsLimitExceededError:
+3 -1
View File
@@ -346,6 +346,7 @@ class EmailCodeLoginApi(Resource):
else:
TenantService.create_owner_tenant(account, session=db.session())
ip_address = extract_remote_ip(request)
if account is None:
try:
account = AccountService.create_account_and_tenant(
@@ -353,6 +354,7 @@ class EmailCodeLoginApi(Resource):
name=user_email,
interface_language=get_valid_language(language),
timezone=req_data.timezone,
ip_address=ip_address,
session=db.session(),
)
except WorkSpaceNotAllowedCreateError:
@@ -364,7 +366,7 @@ class EmailCodeLoginApi(Resource):
raise AccountInFreezeError()
except WorkspacesLimitExceededError:
raise WorkspacesLimitExceeded()
token_pair = AccountService.login(account, session=db.session(), ip_address=extract_remote_ip(request))
token_pair = AccountService.login(account, session=db.session(), ip_address=ip_address)
AccountService.reset_login_error_rate_limit(user_email)
# Create response with cookies instead of returning tokens in body
+9 -1
View File
@@ -233,7 +233,13 @@ class OAuthCallback(Resource):
return _redirect_with_console_session(account, target_url)
try:
account, oauth_new_user = _generate_account(provider, user_info, timezone=timezone, language=language)
account, oauth_new_user = _generate_account(
provider,
user_info,
timezone=timezone,
language=language,
ip_address=extract_remote_ip(request),
)
except AccountNotFoundError:
return redirect(f"{dify_config.CONSOLE_WEB_URL}/signin?message=Account not found.")
except (WorkSpaceNotFoundError, WorkSpaceNotAllowedCreateError):
@@ -285,6 +291,7 @@ def _generate_account(
user_info: OAuthUserInfo,
timezone: str | None = None,
language: str | None = None,
ip_address: str | None = None,
) -> tuple[Account, bool]:
# Get account by openid or email.
account = _get_account_by_openid_or_email(provider, user_info)
@@ -322,6 +329,7 @@ def _generate_account(
provider=provider,
language=interface_language,
timezone=timezone,
ip_address=ip_address,
session=db.session(),
)
+7 -1
View File
@@ -448,6 +448,7 @@ class AccountService:
interface_theme: str = "light",
is_setup: bool | None = False,
timezone: str | None = None,
ip_address: str | None = None,
*,
session: Session,
) -> Account:
@@ -500,6 +501,7 @@ class AccountService:
interface_language=interface_language,
interface_theme=interface_theme,
timezone=resolved_timezone,
last_login_ip=ip_address,
)
session.add(account)
@@ -513,6 +515,7 @@ class AccountService:
interface_language: str,
password: str | None = None,
timezone: str | None = None,
ip_address: str | None = None,
*,
session: Session,
) -> Account:
@@ -523,6 +526,7 @@ class AccountService:
interface_language=interface_language,
password=password,
timezone=timezone,
ip_address=ip_address,
session=session,
)
@@ -1961,10 +1965,10 @@ class RegisterService:
interface_language=get_valid_language(language),
password=password,
is_setup=True,
ip_address=ip_address,
session=session,
)
account.last_login_ip = ip_address
account.initialized_at = naive_utc_now()
TenantService.create_owner_tenant_if_not_exist(account=account, is_setup=True, session=session)
@@ -2000,6 +2004,7 @@ class RegisterService:
is_setup: bool | None = False,
create_workspace_required: bool | None = True,
timezone: str | None = None,
ip_address: str | None = None,
*,
session: Session,
) -> Account:
@@ -2014,6 +2019,7 @@ class RegisterService:
password=password,
is_setup=is_setup,
timezone=timezone,
ip_address=ip_address,
session=session,
)
account.status = status or AccountStatus.ACTIVE
@@ -154,6 +154,7 @@ class TestEmailRegisterResetApi:
password="ValidPass123!",
timezone=None,
language=None,
ip_address="127.0.0.1",
)
mock_reset_login_rate.assert_called_once_with("invitee@example.com")
mock_revoke_token.assert_called_once_with("token-123")
@@ -211,6 +212,7 @@ class TestEmailRegisterResetApi:
password="ValidPass123!",
timezone="Asia/Shanghai",
language=None,
ip_address="127.0.0.1",
)
mock_reset_login_rate.assert_called_once_with("invitee@example.com")
mock_revoke_token.assert_called_once_with("token-123")
@@ -268,6 +270,7 @@ class TestEmailRegisterResetApi:
password="ValidPass123!",
timezone=None,
language="zh-Hans",
ip_address="127.0.0.1",
)
mock_reset_login_rate.assert_called_once_with("invitee@example.com")
mock_revoke_token.assert_called_once_with("token-123")
@@ -25,6 +25,7 @@ def test_create_new_account_uses_requested_language(mock_create_account):
password="ValidPass123!",
interface_language="zh-Hans",
timezone="Asia/Shanghai",
ip_address=None,
session=ANY,
)
@@ -462,16 +462,19 @@ class TestEmailCodeLoginApi:
mock_login.return_value = mock_token_pair
# Act
with app.test_request_context(
"/email-code-login/validity",
method="POST",
json={
"email": "newuser@example.com",
"code": encode_code("123456"),
"token": "valid_token",
"language": "en-US",
"timezone": "Asia/Shanghai",
},
with (
patch("controllers.console.auth.login.extract_remote_ip", return_value="203.0.113.10"),
app.test_request_context(
"/email-code-login/validity",
method="POST",
json={
"email": "newuser@example.com",
"code": encode_code("123456"),
"token": "valid_token",
"language": "en-US",
"timezone": "Asia/Shanghai",
},
),
):
api = EmailCodeLoginApi()
response = api.post()
@@ -483,6 +486,7 @@ class TestEmailCodeLoginApi:
name="newuser@example.com",
interface_language="en-US",
timezone="Asia/Shanghai",
ip_address="203.0.113.10",
session=ANY,
)
@@ -211,11 +211,21 @@ class TestOAuthCallback:
mock_generate_account.return_value = (oauth_setup["account"], True)
mock_account_service.login.return_value = oauth_setup["token_pair"]
with app.test_request_context("/auth/oauth/github/callback?code=test_code"):
with (
patch("controllers.console.auth.oauth.extract_remote_ip", return_value="203.0.113.10"),
app.test_request_context("/auth/oauth/github/callback?code=test_code"),
):
resource.get("github")
oauth_setup["provider"].get_access_token.assert_called_once_with("test_code")
oauth_setup["provider"].get_user_info.assert_called_once_with("access_token")
mock_generate_account.assert_called_once_with(
"github",
oauth_setup["provider"].get_user_info.return_value,
timezone=None,
language=None,
ip_address="203.0.113.10",
)
mock_redirect.assert_called_once_with("http://localhost:3000?oauth_new_user=true")
@pytest.mark.parametrize(
@@ -529,6 +539,7 @@ class TestAccountGeneration:
provider="github",
language="en-US",
timezone=None,
ip_address=None,
session=ANY,
)
else:
@@ -563,6 +574,7 @@ class TestAccountGeneration:
provider="github",
language="en-US",
timezone=None,
ip_address=None,
session=ANY,
)
@@ -595,6 +607,7 @@ class TestAccountGeneration:
provider="github",
language="zh-Hans",
timezone="Asia/Shanghai",
ip_address=None,
session=ANY,
)
@@ -627,6 +640,7 @@ class TestAccountGeneration:
provider="github",
language="zh-Hans",
timezone=None,
ip_address=None,
session=ANY,
)
@@ -56,7 +56,9 @@ def test_generate_account_registers_with_browser_timezone(
user_info = OAuthUserInfo(id="github-123", name="Test User", email="User@Example.com")
with app.test_request_context(headers={"Accept-Language": "zh-Hans,zh;q=0.9"}):
result, oauth_new_user = _generate_account("github", user_info, timezone="Asia/Shanghai")
result, oauth_new_user = _generate_account(
"github", user_info, timezone="Asia/Shanghai", ip_address="203.0.113.10"
)
assert result is account
assert oauth_new_user is True
@@ -68,6 +70,7 @@ def test_generate_account_registers_with_browser_timezone(
provider="github",
language="zh-Hans",
timezone="Asia/Shanghai",
ip_address="203.0.113.10",
session=ANY,
)
mock_link_account.assert_called_once_with("github", "github-123", account, session=ANY)
@@ -100,6 +103,7 @@ def test_generate_account_prefers_state_language_over_accept_language(
provider="github",
language="zh-Hans",
timezone=None,
ip_address=None,
session=ANY,
)
mock_link_account.assert_called_once_with("github", "github-123", account, session=ANY)
@@ -224,6 +224,7 @@ class TestAccountService:
interface_language="en-US",
password="password123",
interface_theme="light",
ip_address="203.0.113.10",
session=service_session,
)
account_id = result.id
@@ -235,6 +236,7 @@ class TestAccountService:
assert result.password is not None
assert result.password_salt is not None
assert result.timezone == "America/New_York"
assert result.last_login_ip == "203.0.113.10"
with sqlite_session_factory() as assertion_session:
persisted_account = assertion_session.get(Account, account_id)
@@ -246,6 +248,7 @@ class TestAccountService:
assert persisted_account.password is not None
assert persisted_account.password_salt is not None
assert persisted_account.timezone == "America/New_York"
assert persisted_account.last_login_ip == "203.0.113.10"
def test_create_account_uses_explicit_timezone(
self,
@@ -338,6 +341,7 @@ class TestAccountService:
assert result.password is None
assert result.password_salt is None
assert result.timezone is not None
assert result.last_login_ip is None
with sqlite_session_factory() as assertion_session:
persisted_account = assertion_session.get(Account, account_id)
@@ -349,6 +353,33 @@ class TestAccountService:
assert persisted_account.password is None
assert persisted_account.password_salt is None
assert persisted_account.timezone is not None
assert persisted_account.last_login_ip is None
def test_update_login_info_overwrites_initial_registration_ip(
self,
sqlite_session_factory: sessionmaker[Session],
mock_external_service_dependencies: _MockDependencies,
) -> None:
mock_external_service_dependencies["feature_service"].get_system_features.return_value.is_allow_register = True
mock_external_service_dependencies["billing_service"].is_email_in_freeze.return_value = False
with sqlite_session_factory() as service_session:
account = AccountService.create_account(
email="test@example.com",
name="Test User",
interface_language="en-US",
ip_address="203.0.113.10",
session=service_session,
)
account_id = account.id
AccountService.update_login_info(account, service_session, ip_address="203.0.113.11")
with sqlite_session_factory() as assertion_session:
persisted_account = assertion_session.get(Account, account_id)
assert persisted_account is not None
assert persisted_account.last_login_ip == "203.0.113.11"
assert persisted_account.last_login_at is not None
# ==================== Password Management Tests ====================
@@ -1448,6 +1479,7 @@ class TestRegisterService:
interface_language="en-US",
password="password123",
is_setup=True,
ip_address="192.168.1.1",
session=service_session,
)
mock_create_tenant.assert_called_once_with(
@@ -1555,10 +1587,20 @@ class TestRegisterService:
name="Test User",
interface_language="en-US",
password=None,
ip_address="203.0.113.10",
session=sqlite_session,
)
assert result == mock_account
mock_create_account.assert_called_once_with(
email="test@example.com",
name="Test User",
interface_language="en-US",
password=None,
timezone=None,
ip_address="203.0.113.10",
session=sqlite_session,
)
mock_create_workspace.assert_called_once_with(account=mock_account, session=sqlite_session)
mock_join_default_workspace.assert_called_once_with(mock_account.id)
@@ -1658,6 +1700,7 @@ class TestRegisterService:
name="Test User",
password="password123",
language="en-US",
ip_address="203.0.113.10",
session=sqlite_session,
)
@@ -1672,6 +1715,7 @@ class TestRegisterService:
password="password123",
is_setup=False,
timezone=None,
ip_address="203.0.113.10",
session=sqlite_session,
)
mock_create_owner_tenant.assert_called_once_with(mock_account, session=sqlite_session)