Skip to content

Commit 4fecd47

Browse files
committed
feat: enhance login settings with account lockout and permission messages
1 parent a7bb173 commit 4fecd47

File tree

8 files changed

+328
-78
lines changed

8 files changed

+328
-78
lines changed

apps/locales/en_US/LC_MESSAGES/django.po

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8865,4 +8865,13 @@ msgid "Super-humanoid: Lingfeibo Pro"
88658865
msgstr ""
88668866

88678867
msgid "Super-humanoid: Lingyuyan Pro"
8868+
msgstr ""
8869+
8870+
msgid "Login failed %s times, account will be locked, you have %s more chances !"
8871+
msgstr ""
8872+
8873+
msgid "This account has been locked for %s minutes, please try again later"
8874+
msgstr ""
8875+
8876+
msgid "User does not have permission to use API Key"
88688877
msgstr ""

apps/locales/zh_CN/LC_MESSAGES/django.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8991,4 +8991,13 @@ msgid "Super-humanoid: Lingfeibo Pro"
89918991
msgstr "聆飞博"
89928992

89938993
msgid "Super-humanoid: Lingyuyan Pro"
8994-
msgstr "聆玉言"
8994+
msgstr "聆玉言"
8995+
8996+
msgid "Login failed %s times, account will be locked, you have %s more chances !"
8997+
msgstr "登录失败 %s 次,账号将被锁定,您还有 %s 次机会!"
8998+
8999+
msgid "This account has been locked for %s minutes, please try again later"
9000+
msgstr "该账号已被锁定 %s 分钟,请稍后再试"
9001+
9002+
msgid "User does not have permission to use API Key"
9003+
msgstr "用户没有使用 API Key 的权限"

apps/locales/zh_Hant/LC_MESSAGES/django.po

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8991,4 +8991,13 @@ msgid "Super-humanoid: Lingfeibo Pro"
89918991
msgstr "聆飛博"
89928992

89938993
msgid "Super-humanoid: Lingyuyan Pro"
8994-
msgstr "聆玉言"
8994+
msgstr "聆玉言"
8995+
8996+
msgid "Login failed %s times, account will be locked, you have %s more chances !"
8997+
msgstr "登录失败 %s 次,账号将被锁定,您还有 %s 次机会!"
8998+
8999+
msgid "This account has been locked for %s minutes, please try again later"
9000+
msgstr "該帳號已被鎖定 %s 分鐘,請稍後再試"
9001+
9002+
msgid "User does not have permission to use API Key"
9003+
msgstr "使用者沒有使用 API Key 的權限"

apps/users/serializers/login.py

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -77,58 +77,134 @@ def get_auth_setting():
7777

7878
@staticmethod
7979
def login(instance):
80+
# 解密数据
8081
username = instance.get("username", "")
81-
encryptedData = instance.get("encryptedData", "")
82-
if encryptedData:
83-
json_data = json.loads(decrypt(encryptedData))
84-
instance.update(json_data)
82+
encrypted_data = instance.get("encryptedData", "")
83+
84+
if encrypted_data:
85+
decrypted_data = json.loads(decrypt(encrypted_data))
86+
instance.update(decrypted_data)
8587
try:
8688
LoginRequest(data=instance).is_valid(raise_exception=True)
89+
except serializers.ValidationError:
90+
raise
8791
except Exception as e:
88-
record_login_fail(username)
89-
raise e
90-
auth_setting = LoginSerializer.get_auth_setting()
92+
raise AppApiException(500, str(e))
9193

92-
max_attempts = auth_setting.get("max_attempts", 1)
9394
password = instance.get("password")
9495
captcha = instance.get("captcha", "")
9596

96-
# 判断是否需要验证码
97-
need_captcha = False
98-
if max_attempts == -1:
99-
need_captcha = False
100-
elif max_attempts > 0:
101-
fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0
102-
need_captcha = fail_count >= max_attempts
103-
104-
if need_captcha:
105-
if not captcha:
106-
raise AppApiException(1005, _("Captcha is required"))
107-
108-
captcha_cache = cache.get(
109-
Cache_Version.CAPTCHA.get_key(captcha=f"system_{username}"),
110-
version=Cache_Version.CAPTCHA.get_version()
111-
)
112-
if captcha_cache is None or captcha.lower() != captcha_cache:
113-
raise AppApiException(1005, _("Captcha code error or expiration"))
97+
# 获取认证配置
98+
auth_setting = LoginSerializer.get_auth_setting()
99+
max_attempts = auth_setting.get("max_attempts", 1)
100+
failed_attempts = auth_setting.get("failed_attempts", 5)
101+
lock_time = auth_setting.get("lock_time", 10)
102+
103+
# 检查许可证有效性
104+
license_validator = DatabaseModelManage.get_model('license_is_valid') or (lambda: False)
105+
is_license_valid = license_validator() if license_validator() is not None else False
106+
107+
if is_license_valid:
108+
# 检查账户是否被锁定
109+
if LoginSerializer._is_account_locked(username):
110+
raise AppApiException(
111+
1005,
112+
_("This account has been locked for %s minutes, please try again later") % lock_time
113+
)
114+
115+
# 验证验证码
116+
if LoginSerializer._need_captcha(username, max_attempts):
117+
LoginSerializer._validate_captcha(username, captcha)
118+
119+
# 验证用户凭据
120+
user = QuerySet(User).filter(
121+
username=username,
122+
password=password_encrypt(password)
123+
).first()
114124

115-
user = QuerySet(User).filter(username=username, password=password_encrypt(password)).first()
116-
if user is None:
117-
record_login_fail(username)
125+
if not user:
126+
LoginSerializer._handle_failed_login(username, is_license_valid, failed_attempts, lock_time)
118127
raise AppApiException(500, _('The username or password is incorrect'))
128+
119129
if not user.is_active:
120-
record_login_fail(username)
121130
raise AppApiException(1005, _("The user has been disabled, please contact the administrator!"))
131+
132+
# 清除失败计数并生成令牌
122133
cache.delete(system_get_key(f'system_{username}'), version=system_version)
123-
token = signing.dumps({'username': user.username,
124-
'id': str(user.id),
125-
'email': user.email,
126-
'type': AuthenticationType.SYSTEM_USER.value})
134+
cache.delete(system_get_key(f'system_{username}_lock'), version=system_version)
135+
token = signing.dumps({
136+
'username': user.username,
137+
'id': str(user.id),
138+
'email': user.email,
139+
'type': AuthenticationType.SYSTEM_USER.value
140+
})
141+
127142
version, get_key = Cache_Version.TOKEN.value
128143
timeout = CONFIG.get_session_timeout()
129144
cache.set(get_key(token), user, timeout=timeout, version=version)
145+
130146
return {'token': token}
131147

148+
@staticmethod
149+
def _is_account_locked(username: str) -> bool:
150+
"""检查账户是否被锁定"""
151+
lock_cache = cache.get(system_get_key(f'system_{username}_lock'), version=system_version)
152+
return bool(lock_cache)
153+
154+
@staticmethod
155+
def _need_captcha(username: str, max_attempts: int) -> bool:
156+
"""判断是否需要验证码"""
157+
if max_attempts == -1:
158+
return False
159+
elif max_attempts > 0:
160+
fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0
161+
return fail_count >= max_attempts
162+
return True
163+
164+
@staticmethod
165+
def _validate_captcha(username: str, captcha: str) -> None:
166+
"""验证验证码"""
167+
if not captcha:
168+
raise AppApiException(1005, _("Captcha is required"))
169+
170+
captcha_cache = cache.get(
171+
Cache_Version.CAPTCHA.get_key(captcha=f"system_{username}"),
172+
version=Cache_Version.CAPTCHA.get_version()
173+
)
174+
175+
if captcha_cache is None or captcha.lower() != captcha_cache:
176+
raise AppApiException(1005, _("Captcha code error or expiration"))
177+
178+
@staticmethod
179+
def _handle_failed_login(username: str, is_license_valid: bool, failed_attempts: int, lock_time: int) -> None:
180+
"""处理登录失败"""
181+
record_login_fail(username)
182+
183+
if not is_license_valid or failed_attempts <= 0:
184+
return
185+
186+
fail_count = cache.get(system_get_key(f'system_{username}'), version=system_version) or 0
187+
remain_attempts = failed_attempts - fail_count
188+
189+
if remain_attempts > 0:
190+
raise AppApiException(
191+
1005,
192+
_("Login failed %s times, account will be locked, you have %s more chances !") % (
193+
failed_attempts, remain_attempts
194+
)
195+
)
196+
elif remain_attempts == 0:
197+
cache.set(
198+
system_get_key(f'system_{username}_lock'),
199+
1,
200+
timeout=lock_time * 60,
201+
version=system_version
202+
)
203+
raise AppApiException(
204+
1005,
205+
_("This account has been locked for %s minutes, please try again later") % lock_time
206+
)
207+
132208

133209
class CaptchaResponse(serializers.Serializer):
134210
"""
@@ -171,7 +247,6 @@ def chat_generate(username: str, type: str = 'chat', access_token: str = ''):
171247
fail_count = cache.get(system_get_key(f'{type}_{username}'), version=system_version) or 0
172248
need_captcha = fail_count >= max_attempts
173249

174-
175250
return CaptchaSerializer._generate_captcha_if_needed(username, type, need_captcha)
176251

177252
@staticmethod

ui/src/locales/lang/en-US/views/system.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,4 +159,5 @@ export default {
159159
setting: 'Login Setting',
160160
failedTip: 'Next, lock the account',
161161
minute: 'Minutes',
162+
third_party_user_default_role: 'Default Role Assignment for Third-party Users',
162163
}

ui/src/locales/lang/zh-CN/views/system.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ export default {
160160
display_codeTip: '值为-1时,不显示验证码',
161161
time: '次',
162162
setting: '登录设置',
163+
third_party_user_default_role: '第三方用户默认角色分配',
163164
}

ui/src/locales/lang/zh-Hant/views/system.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ export default {
160160
minute: '分鐘',
161161
time: '次',
162162
setting: '登录設置',
163+
third_party_user_default_role: '第三方用戶預設角色分配',
163164
}

0 commit comments

Comments
 (0)