From 4fecd47904795a7a0ffdfbceaf4d8530477bce2b Mon Sep 17 00:00:00 2001 From: wxg0103 <727495428@qq.com> Date: Fri, 19 Dec 2025 14:16:17 +0800 Subject: [PATCH] feat: enhance login settings with account lockout and permission messages --- apps/locales/en_US/LC_MESSAGES/django.po | 9 + apps/locales/zh_CN/LC_MESSAGES/django.po | 11 +- apps/locales/zh_Hant/LC_MESSAGES/django.po | 11 +- apps/users/serializers/login.py | 141 ++++++++--- ui/src/locales/lang/en-US/views/system.ts | 1 + ui/src/locales/lang/zh-CN/views/system.ts | 1 + ui/src/locales/lang/zh-Hant/views/system.ts | 1 + .../authentication/component/Setting.vue | 227 ++++++++++++++---- 8 files changed, 326 insertions(+), 76 deletions(-) diff --git a/apps/locales/en_US/LC_MESSAGES/django.po b/apps/locales/en_US/LC_MESSAGES/django.po index 619312e45..b222b658c 100644 --- a/apps/locales/en_US/LC_MESSAGES/django.po +++ b/apps/locales/en_US/LC_MESSAGES/django.po @@ -8865,4 +8865,13 @@ msgid "Super-humanoid: Lingfeibo Pro" msgstr "" msgid "Super-humanoid: Lingyuyan Pro" +msgstr "" + +msgid "Login failed %s times, account will be locked, you have %s more chances !" +msgstr "" + +msgid "This account has been locked for %s minutes, please try again later" +msgstr "" + +msgid "User does not have permission to use API Key" msgstr "" \ No newline at end of file diff --git a/apps/locales/zh_CN/LC_MESSAGES/django.po b/apps/locales/zh_CN/LC_MESSAGES/django.po index 07f9994aa..733b3a3e0 100644 --- a/apps/locales/zh_CN/LC_MESSAGES/django.po +++ b/apps/locales/zh_CN/LC_MESSAGES/django.po @@ -8991,4 +8991,13 @@ msgid "Super-humanoid: Lingfeibo Pro" msgstr "聆飞博" msgid "Super-humanoid: Lingyuyan Pro" -msgstr "聆玉言" \ No newline at end of file +msgstr "聆玉言" + +msgid "Login failed %s times, account will be locked, you have %s more chances !" +msgstr "登录失败 %s 次,账号将被锁定,您还有 %s 次机会!" + +msgid "This account has been locked for %s minutes, please try again later" +msgstr "该账号已被锁定 %s 分钟,请稍后再试" + +msgid "User does not have permission to use API Key" +msgstr "用户没有使用 API Key 的权限" \ No newline at end of file diff --git a/apps/locales/zh_Hant/LC_MESSAGES/django.po b/apps/locales/zh_Hant/LC_MESSAGES/django.po index 90d1b5ec6..952bf72f0 100644 --- a/apps/locales/zh_Hant/LC_MESSAGES/django.po +++ b/apps/locales/zh_Hant/LC_MESSAGES/django.po @@ -8991,4 +8991,13 @@ msgid "Super-humanoid: Lingfeibo Pro" msgstr "聆飛博" msgid "Super-humanoid: Lingyuyan Pro" -msgstr "聆玉言" \ No newline at end of file +msgstr "聆玉言" + +msgid "Login failed %s times, account will be locked, you have %s more chances !" +msgstr "登录失败 %s 次,账号将被锁定,您还有 %s 次机会!" + +msgid "This account has been locked for %s minutes, please try again later" +msgstr "該帳號已被鎖定 %s 分鐘,請稍後再試" + +msgid "User does not have permission to use API Key" +msgstr "使用者沒有使用 API Key 的權限" diff --git a/apps/users/serializers/login.py b/apps/users/serializers/login.py index e5487f137..b3bddf490 100644 --- a/apps/users/serializers/login.py +++ b/apps/users/serializers/login.py @@ -77,58 +77,134 @@ class LoginSerializer(serializers.Serializer): @staticmethod def login(instance): + # 解密数据 username = instance.get("username", "") - encryptedData = instance.get("encryptedData", "") - if encryptedData: - json_data = json.loads(decrypt(encryptedData)) - instance.update(json_data) + encrypted_data = instance.get("encryptedData", "") + + if encrypted_data: + decrypted_data = json.loads(decrypt(encrypted_data)) + instance.update(decrypted_data) try: LoginRequest(data=instance).is_valid(raise_exception=True) + except serializers.ValidationError: + raise except Exception as e: - record_login_fail(username) - raise e - auth_setting = LoginSerializer.get_auth_setting() + raise AppApiException(500, str(e)) - max_attempts = auth_setting.get("max_attempts", 1) password = instance.get("password") captcha = instance.get("captcha", "") - # 判断是否需要验证码 - need_captcha = False - if max_attempts == -1: - need_captcha = False - elif max_attempts > 0: - fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0 - need_captcha = fail_count >= max_attempts + # 获取认证配置 + auth_setting = LoginSerializer.get_auth_setting() + max_attempts = auth_setting.get("max_attempts", 1) + failed_attempts = auth_setting.get("failed_attempts", 5) + lock_time = auth_setting.get("lock_time", 10) - if need_captcha: - if not captcha: - raise AppApiException(1005, _("Captcha is required")) + # 检查许可证有效性 + license_validator = DatabaseModelManage.get_model('license_is_valid') or (lambda: False) + is_license_valid = license_validator() if license_validator() is not None else False - captcha_cache = cache.get( - Cache_Version.CAPTCHA.get_key(captcha=f"system_{username}"), - version=Cache_Version.CAPTCHA.get_version() - ) - if captcha_cache is None or captcha.lower() != captcha_cache: - raise AppApiException(1005, _("Captcha code error or expiration")) + if is_license_valid: + # 检查账户是否被锁定 + if LoginSerializer._is_account_locked(username): + raise AppApiException( + 1005, + _("This account has been locked for %s minutes, please try again later") % lock_time + ) - user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first() - if user is None: - record_login_fail(username) + # 验证验证码 + if LoginSerializer._need_captcha(username, max_attempts): + LoginSerializer._validate_captcha(username, captcha) + + # 验证用户凭据 + user = QuerySet(User).filter( + username=username, + password=password_encrypt(password) + ).first() + + if not user: + LoginSerializer._handle_failed_login(username, is_license_valid, failed_attempts, lock_time) raise AppApiException(500, _('The username or password is incorrect')) + if not user.is_active: - record_login_fail(username) raise AppApiException(1005, _("The user has been disabled, please contact the administrator!")) + + # 清除失败计数并生成令牌 cache.delete(system_get_key(f'system_{username}'), version=system_version) - token = signing.dumps({'username': user.username, - 'id': str(user.id), - 'email': user.email, - 'type': AuthenticationType.SYSTEM_USER.value}) + cache.delete(system_get_key(f'system_{username}_lock'), version=system_version) + token = signing.dumps({ + 'username': user.username, + 'id': str(user.id), + 'email': user.email, + 'type': AuthenticationType.SYSTEM_USER.value + }) + version, get_key = Cache_Version.TOKEN.value timeout = CONFIG.get_session_timeout() cache.set(get_key(token), user, timeout=timeout, version=version) + return {'token': token} + @staticmethod + def _is_account_locked(username: str) -> bool: + """检查账户是否被锁定""" + lock_cache = cache.get(system_get_key(f'system_{username}_lock'), version=system_version) + return bool(lock_cache) + + @staticmethod + def _need_captcha(username: str, max_attempts: int) -> bool: + """判断是否需要验证码""" + if max_attempts == -1: + return False + elif max_attempts > 0: + fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0 + return fail_count >= max_attempts + return True + + @staticmethod + def _validate_captcha(username: str, captcha: str) -> None: + """验证验证码""" + if not captcha: + raise AppApiException(1005, _("Captcha is required")) + + captcha_cache = cache.get( + Cache_Version.CAPTCHA.get_key(captcha=f"system_{username}"), + version=Cache_Version.CAPTCHA.get_version() + ) + + if captcha_cache is None or captcha.lower() != captcha_cache: + raise AppApiException(1005, _("Captcha code error or expiration")) + + @staticmethod + def _handle_failed_login(username: str, is_license_valid: bool, failed_attempts: int, lock_time: int) -> None: + """处理登录失败""" + record_login_fail(username) + + if not is_license_valid or failed_attempts <= 0: + return + + fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0 + remain_attempts = failed_attempts - fail_count + + if remain_attempts > 0: + raise AppApiException( + 1005, + _("Login failed %s times, account will be locked, you have %s more chances !") % ( + failed_attempts, remain_attempts + ) + ) + elif remain_attempts == 0: + cache.set( + system_get_key(f'system_{username}_lock'), + 1, + timeout=lock_time * 60, + version=system_version + ) + raise AppApiException( + 1005, + _("This account has been locked for %s minutes, please try again later") % lock_time + ) + class CaptchaResponse(serializers.Serializer): """ @@ -171,7 +247,6 @@ class CaptchaSerializer(serializers.Serializer): fail_count = cache.get(system_get_key(f'{type}_{username}'), version=system_version) or 0 need_captcha = fail_count >= max_attempts - return CaptchaSerializer._generate_captcha_if_needed(username, type, need_captcha) @staticmethod diff --git a/ui/src/locales/lang/en-US/views/system.ts b/ui/src/locales/lang/en-US/views/system.ts index 76df36aa1..218293bb5 100644 --- a/ui/src/locales/lang/en-US/views/system.ts +++ b/ui/src/locales/lang/en-US/views/system.ts @@ -159,4 +159,5 @@ export default { setting: 'Login Setting', failedTip: 'Next, lock the account', minute: 'Minutes', + third_party_user_default_role: 'Default Role Assignment for Third-party Users', } diff --git a/ui/src/locales/lang/zh-CN/views/system.ts b/ui/src/locales/lang/zh-CN/views/system.ts index 29f29fed7..9dc096468 100644 --- a/ui/src/locales/lang/zh-CN/views/system.ts +++ b/ui/src/locales/lang/zh-CN/views/system.ts @@ -160,4 +160,5 @@ export default { display_codeTip: '值为-1时,不显示验证码', time: '次', setting: '登录设置', + third_party_user_default_role: '第三方用户默认角色分配', } diff --git a/ui/src/locales/lang/zh-Hant/views/system.ts b/ui/src/locales/lang/zh-Hant/views/system.ts index c52a919a9..63663260e 100644 --- a/ui/src/locales/lang/zh-Hant/views/system.ts +++ b/ui/src/locales/lang/zh-Hant/views/system.ts @@ -160,4 +160,5 @@ export default { minute: '分鐘', time: '次', setting: '登录設置', + third_party_user_default_role: '第三方用戶預設角色分配', } diff --git a/ui/src/views/system-setting/authentication/component/Setting.vue b/ui/src/views/system-setting/authentication/component/Setting.vue index aa6e3a38a..52c4ba310 100644 --- a/ui/src/views/system-setting/authentication/component/Setting.vue +++ b/ui/src/views/system-setting/authentication/component/Setting.vue @@ -17,7 +17,8 @@ prop="default_value" style="padding-top: 16px" > - + - + {{ $t('views.system.loginFailed') }} @@ -52,6 +53,7 @@ :max="10" :step="1" controls-position="right" + @change="onMaxAttemptsChange" /> {{ $t('views.system.loginFailedMessage') }} @@ -72,25 +74,78 @@ :max="10" :step="1" controls-position="right" + @change="onFailedAttemptsChange" /> - {{ $t('views.system.loginFailedMessage') }} + {{ $t('views.system.failedTip') }} - 分钟 + {{ $t('views.system.minute') }} + + + +
+ + {{ $t('views.role.member.role') }} + + + + +
+
+ +
+ + {{ $t('views.role.member.workspace') }} + + + + +
+
+
+ +
- - {{ $t('common.save') }} - - + + + {{ $t('common.save') }} + +
@@ -110,58 +166,147 @@