mirror of
https://github.com/langgenius/dify.git
synced 2026-08-31 01:36:38 +08:00
feat: create accounts with client IP (#40875)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user