mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix: Telegram pairing approval required for existing bots / existing pairing skipped on reconfigure
This commit is contained in:
@@ -2640,7 +2640,7 @@ function renderExtensionCard(ext) {
|
||||
pairingSection.className = 'ext-pairing';
|
||||
pairingSection.setAttribute('data-channel', ext.name);
|
||||
card.appendChild(pairingSection);
|
||||
loadPairingRequests(ext.name, pairingSection);
|
||||
loadPairingRequests(ext.name, pairingSection, ext.activation_status);
|
||||
}
|
||||
|
||||
return card;
|
||||
@@ -3034,11 +3034,19 @@ function openOAuthUrl(url) {
|
||||
|
||||
// --- Pairing ---
|
||||
|
||||
function loadPairingRequests(channel, container) {
|
||||
function loadPairingRequests(channel, container, status) {
|
||||
apiFetch('/api/pairing/' + encodeURIComponent(channel))
|
||||
.then(data => {
|
||||
container.innerHTML = '';
|
||||
if (!data.requests || data.requests.length === 0) return;
|
||||
if (!data.requests || data.requests.length === 0) {
|
||||
if (status === 'pairing') {
|
||||
const hint = document.createElement('p');
|
||||
hint.className = 'pairing-hint';
|
||||
hint.textContent = 'Send any message to your bot to receive a pairing request here.';
|
||||
container.appendChild(hint);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const heading = document.createElement('div');
|
||||
heading.className = 'pairing-heading';
|
||||
|
||||
@@ -2865,6 +2865,13 @@ body {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pairing-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
margin: 4px 0 8px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Configure modal */
|
||||
.configure-overlay {
|
||||
position: fixed;
|
||||
|
||||
@@ -3739,6 +3739,26 @@ impl ExtensionManager {
|
||||
}
|
||||
};
|
||||
|
||||
// Credentials changed (new bot token) — clear pairing state so existing users
|
||||
// must re-approve with the new bot identity.
|
||||
if cred_count > 0 {
|
||||
let pairing_store = crate::pairing::PairingStore::new();
|
||||
if let Err(e) = pairing_store.clear_allow_from(name) {
|
||||
tracing::warn!(
|
||||
channel = %name,
|
||||
error = %e,
|
||||
"Failed to clear allow-from on credential refresh"
|
||||
);
|
||||
}
|
||||
if let Err(e) = pairing_store.clear_pending(name) {
|
||||
tracing::warn!(
|
||||
channel = %name,
|
||||
error = %e,
|
||||
"Failed to clear pending pairings on credential refresh"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Load capabilities file once to extract all secret names
|
||||
let cap_path = self
|
||||
.wasm_channels_dir
|
||||
|
||||
@@ -440,6 +440,39 @@ impl PairingStore {
|
||||
Ok(file.allow_from)
|
||||
}
|
||||
|
||||
/// Clear the allow-from list for a channel.
|
||||
///
|
||||
/// Called on credential refresh so that existing users must re-approve
|
||||
/// after a bot token change.
|
||||
pub fn clear_allow_from(&self, channel: &str) -> Result<(), PairingStoreError> {
|
||||
let path = allow_from_path(&self.base_dir, channel)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&path)?;
|
||||
file.lock_exclusive()?;
|
||||
let store = AllowFromStoreFile {
|
||||
version: 1,
|
||||
allow_from: Vec::new(),
|
||||
};
|
||||
let json = serde_json::to_string_pretty(&store)?;
|
||||
fs::write(&path, json)?;
|
||||
fs4::FileExt::unlock(&file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Clear all pending pairing requests for a channel.
|
||||
///
|
||||
/// Called on credential refresh so stale requests don't confuse users.
|
||||
pub fn clear_pending(&self, channel: &str) -> Result<(), PairingStoreError> {
|
||||
self.write_pairing_file(channel, &[])
|
||||
}
|
||||
|
||||
/// Check if a sender is allowed (by id or username).
|
||||
pub fn is_sender_allowed(
|
||||
&self,
|
||||
@@ -717,4 +750,49 @@ mod tests {
|
||||
store.list_pending("").unwrap_err();
|
||||
store.upsert_request("", "u1", None).unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_allow_from_removes_all_entries() {
|
||||
let (store, _) = test_store();
|
||||
let r1 = store.upsert_request("telegram", "user1", None).unwrap();
|
||||
store.approve("telegram", &r1.code).unwrap();
|
||||
|
||||
let list = store.read_allow_from("telegram").unwrap();
|
||||
assert_eq!(list.len(), 1);
|
||||
|
||||
store.clear_allow_from("telegram").unwrap();
|
||||
let list = store.read_allow_from("telegram").unwrap();
|
||||
assert!(list.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_pending_removes_all_requests() {
|
||||
let (store, _) = test_store();
|
||||
store
|
||||
.upsert_request("telegram", "user1", Some(serde_json::json!({"chat_id": 1})))
|
||||
.unwrap();
|
||||
store
|
||||
.upsert_request("telegram", "user2", Some(serde_json::json!({"chat_id": 2})))
|
||||
.unwrap();
|
||||
|
||||
let requests = store.list_pending("telegram").unwrap();
|
||||
assert_eq!(requests.len(), 2);
|
||||
|
||||
store.clear_pending("telegram").unwrap();
|
||||
let requests = store.list_pending("telegram").unwrap();
|
||||
assert!(requests.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clear_allow_from_allows_new_approval() {
|
||||
let (store, _) = test_store();
|
||||
let r1 = store.upsert_request("telegram", "user1", None).unwrap();
|
||||
store.approve("telegram", &r1.code).unwrap();
|
||||
|
||||
assert!(store.is_sender_allowed("telegram", "user1", None).unwrap());
|
||||
|
||||
store.clear_allow_from("telegram").unwrap();
|
||||
|
||||
assert!(!store.is_sender_allowed("telegram", "user1", None).unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user