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:
@@ -56,6 +56,22 @@ def auth_status():
|
|||||||
return jsonify({"hasToken": garmin_svc.has_token(g.user_id)})
|
return jsonify({"hasToken": garmin_svc.has_token(g.user_id)})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route("/disconnect", methods=["POST"])
|
||||||
|
@require_auth
|
||||||
|
def disconnect():
|
||||||
|
"""Drop the stored Garmin token so the next sync must re-authenticate.
|
||||||
|
|
||||||
|
Deletes the OAuth token (the UI calls this a "退出 Garmin 账号"). garmin_email
|
||||||
|
stays on the user record, so re-login only needs the password again. Already
|
||||||
|
synced health data is untouched.
|
||||||
|
"""
|
||||||
|
garmin_svc.delete_token(g.user_id)
|
||||||
|
return jsonify({
|
||||||
|
"ok": True,
|
||||||
|
"message": "已退出 Garmin 账号,已保存的授权令牌已删除,下次同步需重新登录获取新令牌。",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.route("/login", methods=["POST"])
|
@bp.route("/login", methods=["POST"])
|
||||||
@require_auth
|
@require_auth
|
||||||
def login():
|
def login():
|
||||||
|
|||||||
@@ -257,6 +257,18 @@ def has_token(user_id):
|
|||||||
return load_token(user_id) is not None
|
return load_token(user_id) is not None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_token(user_id):
|
||||||
|
"""Forget the stored Garmin OAuth token.
|
||||||
|
|
||||||
|
The next sync or login will have to re-authenticate and mint a fresh token.
|
||||||
|
garmin_email on the user record is left in place so re-login only needs the
|
||||||
|
password. The cached client — built from the old token's session — is dropped
|
||||||
|
in the same step so a stale session can't keep being reused.
|
||||||
|
"""
|
||||||
|
execute("DELETE FROM garmin_tokens WHERE user_id = ?", [user_id])
|
||||||
|
forget_client(user_id)
|
||||||
|
|
||||||
|
|
||||||
# An authenticated client, reused across requests in this process.
|
# An authenticated client, reused across requests in this process.
|
||||||
#
|
#
|
||||||
# Building one costs ~11s against Garmin — loading the token, refreshing the
|
# Building one costs ~11s against Garmin — loading the token, refreshing the
|
||||||
|
|||||||
@@ -281,3 +281,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sync-list li { margin-bottom: 0.2rem; }
|
.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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -164,6 +164,30 @@ function SyncPage() {
|
|||||||
setCodeSubmitted(false);
|
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 ---------------------------------------------------------------
|
// --- sync ---------------------------------------------------------------
|
||||||
|
|
||||||
/** The last couple of days, awaited inline — seconds, not minutes. */
|
/** The last couple of days, awaited inline — seconds, not minutes. */
|
||||||
@@ -382,6 +406,19 @@ function SyncPage() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="sync-account">
|
||||||
|
<button
|
||||||
|
className="btn btn-plain sync-disconnect"
|
||||||
|
onClick={disconnect}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
退出 Garmin 账号
|
||||||
|
</button>
|
||||||
|
<p className="field-hint">
|
||||||
|
删除已保存的授权令牌,下次同步需重新登录获取新令牌;已同步的数据不会丢失。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -467,6 +467,14 @@ class ApiClient {
|
|||||||
return data.hasToken;
|
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
|
* 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.
|
* in the background and parks if Garmin asks for a two-factor code.
|
||||||
|
|||||||
Reference in New Issue
Block a user