feat(garmin): 增加「退出 Garmin 账号」功能,删除已保存令牌以便重新登录

backend: services/garmin.py 新增 delete_token(),删除 garmin_tokens 行并 forget_client 丢弃缓存会话;routes/garmin.py 新增 POST /api/garmin/disconnect(require_auth),返回 ok + 提示文案。
frontend: api.ts 增加 disconnectGarmin();SyncPage 增加「退出 Garmin 账号」按钮(带二次确认弹窗);DataSync.css 增加危险色样式。
设计:仅删除 OAuth 令牌,保留 users.garmin_email,下次重登只需密码;已同步的健康数据不受影响。
This commit is contained in:
ericwyuan
2026-08-28 15:13:32 +08:00
parent 11869c6a8d
commit bec45414a7
5 changed files with 91 additions and 0 deletions

View File

@@ -281,3 +281,21 @@
}
.sync-list li { margin-bottom: 0.2rem; }
/* Account actions — destructive ones are flagged in red so they don't read
like the routine sync buttons above them. */
.sync-account {
margin-top: 1.4rem;
padding-top: 1.1rem;
border-top: 1px solid var(--border);
}
.sync-disconnect {
color: var(--danger, #d23b3b);
border-color: var(--danger, #d23b3b);
}
.sync-disconnect:hover:not(:disabled) {
background: var(--danger, #d23b3b);
color: #fff;
}

View File

@@ -164,6 +164,30 @@ function SyncPage() {
setCodeSubmitted(false);
};
// --- account ------------------------------------------------------------
/** Delete the stored Garmin token so the next sync forces a fresh login. */
const disconnect = async () => {
if (
!window.confirm(
'退出 Garmin 账号会删除已保存的授权令牌,下次同步需重新登录。确定继续?'
)
) {
return;
}
setError('');
setMessage('');
setLoading(true);
try {
const res = await apiClient.disconnectGarmin();
setHasToken(false);
setMessage(res.message || '已退出 Garmin 账号,下次同步需重新登录获取新令牌。');
} catch (err: any) {
setError(errorMessage(err, '退出失败'));
} finally {
setLoading(false);
}
};
// --- sync ---------------------------------------------------------------
/** The last couple of days, awaited inline — seconds, not minutes. */
@@ -382,6 +406,19 @@ function SyncPage() {
</span>
</button>
</div>
<div className="sync-account">
<button
className="btn btn-plain sync-disconnect"
onClick={disconnect}
disabled={busy}
>
退 Garmin
</button>
<p className="field-hint">
</p>
</div>
</>
)}

View File

@@ -467,6 +467,14 @@ class ApiClient {
return data.hasToken;
}
/** Drop the stored Garmin OAuth token; the next sync must re-authenticate. */
async disconnectGarmin() {
const { data } = await this.client.post<{ ok: boolean; message?: string }>(
'/garmin/disconnect'
);
return data;
}
/**
* Start an interactive Garmin login. Returns a session id; the login runs
* in the background and parks if Garmin asks for a two-factor code.