fix(garmin): 重新绑定走的是同一个被封的登录接口,也得拦住
用户问:限流了,重新输账号密码验证码换个新令牌行不行。
不行,而且是最糟的一种试法。`garth.login()` 和 `refresh_oauth2()` 打的是
同一个 SSO 端点,流程还更重;限流按**账号**计(不是按 IP、按 UA),换设备
换网络都绕不开;而窗口内每次尝试都会把窗口往后推。
而这正是被卡住时第一个会去试的操作,代码里却只有 `_connect` 的刷新有闸门,
重新绑定那条路照发不误。
- start_login 在 sso 冷却窗口内直接拒绝,不建会话行、不碰网络
- 错误信息说清三件事:为什么现在不试、什么时候恢复、换设备没用
- 路由返 429(请求本身没毛病,是该晚点再来)并带 retryAfterSeconds
- 数据端点的 429 不参与拦截,force 可以推翻
前端补上 UI:报错文案早先承诺了「同步页选择强制重试」,但那个按钮不存在。
现在只在被冷却拒绝之后才出现,样式刻意做得不像第二个「开始同步」——它是给
估算失准时的出口,不是随手可点的第二选择。
顺带修一个正要被我引入的 bug:`onClick={syncHistory}` 会把 MouseEvent 当成
force 传进去,等于每次点开始同步都跳过冷却。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -129,8 +129,47 @@ def _run_login(session_id, user_id, garmin_email, password, is_cn, import_garmin
|
||||
_set(session_id, "failed", error=error, code=None)
|
||||
|
||||
|
||||
def start_login(user_id, garmin_email, password, import_garmin=None, is_cn=None):
|
||||
"""Kick off a login in the background. Returns the session id."""
|
||||
class LoginRateLimited(Exception):
|
||||
"""Refused before contacting Garmin, because a login 429 is still active."""
|
||||
|
||||
|
||||
def retry_after_seconds(user_id):
|
||||
"""Seconds until the login cooldown lapses, or None."""
|
||||
blocked = garmin_svc.sso_cooldown(user_id)
|
||||
if not blocked:
|
||||
return None
|
||||
return max(0, int((blocked - datetime.datetime.utcnow()).total_seconds()))
|
||||
|
||||
|
||||
def start_login(user_id, garmin_email, password, import_garmin=None,
|
||||
is_cn=None, force=False):
|
||||
"""Kick off a login in the background. Returns the session id.
|
||||
|
||||
Refuses while a login 429 is still in its window. Re-binding is the
|
||||
obvious thing to try when syncing is blocked — "just get a fresh token" —
|
||||
but it goes through `garth.login()`, which is the *same* SSO endpoint that
|
||||
is doing the blocking, by a heavier path than the token refresh. The limit
|
||||
is keyed to the account, so a new password entry, a new device or a new
|
||||
network reaches the same wall, and each attempt pushes the window out.
|
||||
|
||||
`force` overrules the recorded deadline, which is this app's own 24h guess
|
||||
rather than anything Garmin stated.
|
||||
"""
|
||||
if force:
|
||||
garmin_svc.clear_rate_limit(user_id)
|
||||
else:
|
||||
blocked = garmin_svc.sso_cooldown(user_id)
|
||||
if blocked:
|
||||
minutes = int(
|
||||
(blocked - datetime.datetime.utcnow()).total_seconds() // 60
|
||||
)
|
||||
raise LoginRateLimited(
|
||||
"Garmin 正在限制该账号的登录请求。重新绑定走的是同一个登录接口,"
|
||||
f"现在重试只会延长封锁。预计 {blocked.isoformat(timespec='minutes')} "
|
||||
f"UTC 之后恢复(约 {minutes} 分钟)。"
|
||||
"限流按账号计算,换设备或换网络都绕不开。"
|
||||
)
|
||||
|
||||
_cleanup(user_id)
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
Reference in New Issue
Block a user