collabSupport/commands/resetVpPin.js
jmcqueen 2ec2b7a486 Add /resetvmpin and /resetvppin with confirmation cards for Webex PIN resets.
Chat-only commands reset voicemail mailbox and voice portal passcodes via telephony_config_write, with extension lookup and unit tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 16:34:09 -04:00

186 lines
5.7 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// commands/resetVpPin.js
//
// /resetvppin <store> — reset a location's voice portal PIN with confirmation card.
//
// Store number only (24 digits). Used to manage auto-attendant greetings.
import { logger } from '../utils/logger.js';
import { pendingVpPinResets } from '../utils/pendingVpPinResets.js';
import { extractRequester, describeRequester } from '../utils/requester.js';
import {
parseVpPinStore,
resolveVpPinStore,
} from '../services/voicePin/resolveVpPinStore.js';
import { resetVoicePortalPin } from '../services/voicePin/resetVoicePortalPin.js';
function buildConfirmationCard(cardId, resolved) {
const portal = resolved.voicePortal || {};
const dialIn = portal.phoneNumber
? `${portal.phoneNumber}${portal.extension ? ` ext ${portal.extension}` : ''}`
: '—';
return {
type: 'AdaptiveCard',
version: '1.3',
body: [
{
type: 'TextBlock',
text: '🔐 RESET VOICE PORTAL PIN',
weight: 'Bolder',
size: 'Large',
color: 'Attention',
},
{
type: 'TextBlock',
text:
'Confirming will generate a new voice portal passcode for this location. ' +
'This PIN is used to dial in and manage **auto-attendant greetings**.',
wrap: true,
spacing: 'Small',
},
{
type: 'FactSet',
spacing: 'Medium',
facts: [
{ title: 'Store', value: resolved.storeNum },
{ title: 'Location', value: resolved.locationName || '—' },
{ title: 'Location ID', value: resolved.locationId },
{ title: 'Voice portal', value: portal.name || '—' },
{ title: 'Dial-in', value: dialIn },
{ title: 'Main number', value: resolved.mainNumber || '—' },
],
},
],
actions: [
{
type: 'Action.Submit',
title: '✅ Confirm Reset',
data: { action: 'confirm_reset_vppin', cardId },
},
{
type: 'Action.Submit',
title: '❌ Cancel',
data: { action: 'cancel_reset_vppin', cardId },
},
],
};
}
export async function applyResetVpPinConfirmation(bot, data, _roomId, requester) {
logger(
'resetvppin:audit',
`CONFIRMED voice portal PIN reset for store ${data.storeNum} (location=${data.locationId}) ` +
`by ${describeRequester(requester)}`,
);
try {
const newPin = await resetVoicePortalPin(data.locationId, {
passcodeRules: data.passcodeRules,
voicePortal: data.voicePortal,
});
const portal = data.voicePortal || {};
const dialIn = portal.phoneNumber
? `${portal.phoneNumber}${portal.extension ? ` ext ${portal.extension}` : ''}`
: null;
await bot.say(
'markdown',
`✅ Voice portal PIN reset for **Store ${data.storeNum}** ` +
`(\`${data.locationName || data.locationId}\`).\n\n` +
`**New voice portal PIN:** \`${newPin}\`\n` +
(dialIn ? `\nDial in at **${dialIn}** to manage auto-attendant greetings.\n` : '\n') +
'_Share this PIN only in trusted spaces._',
);
logger(
'resetvppin:audit',
`COMPLETED voice portal PIN reset for store ${data.storeNum} (location=${data.locationId}) ` +
`by ${describeRequester(requester)}`,
);
} catch (err) {
const apiMsg = err?.response?.data?.message || err.message;
logger(
'resetvppin:audit',
`FAILED voice portal PIN reset for store ${data.storeNum}: ${apiMsg}`,
'error',
);
await bot.say(
'markdown',
`❌ Voice portal PIN reset failed for **Store ${data.storeNum}**: ${apiMsg}`,
);
}
}
export async function cancelResetVpPinCard(bot, data, _roomId, requester) {
await bot.say(
'markdown',
`❌ Voice portal PIN reset cancelled for **Store ${data.storeNum}**. No changes were made.`,
);
logger(
'resetvppin:audit',
`CANCELLED voice portal PIN reset for store ${data.storeNum} by ${describeRequester(requester)}`,
);
}
export async function handleResetVpPin(bot, trigger) {
const args = trigger.args || [];
const query = trigger.query || {};
const raw = (args[0] || query.store || query.storeNum || query.s || '').trim();
if (!raw) {
await bot.say(
'markdown',
'**Usage:** `/resetvppin <store>`\n\n' +
'Provide a 24 digit store number (e.g. `782`).',
);
return;
}
const parsed = parseVpPinStore(raw);
if (!parsed) {
await bot.say(
'markdown',
`❌ Invalid store \`${raw}\`. Use a 24 digit store number.`,
);
return;
}
const requester = extractRequester(trigger);
logger(
'resetvppin:audit',
`REQUESTED voice portal PIN reset card for store ${parsed.storeNum} by ${describeRequester(requester)}`,
);
try {
const resolved = await resolveVpPinStore(parsed);
if (!trigger.person) {
await bot.say(
'markdown',
`Resolved **Store ${resolved.storeNum}** → \`${resolved.locationName || resolved.locationId}\`. ` +
'Confirmation cards are only available in Webex chat.',
);
return;
}
const cardId = `vppin-${Date.now()}`;
pendingVpPinResets.set(cardId, {
...resolved,
roomId: trigger.roomId || trigger.message?.roomId,
requester,
});
const adaptiveCard = buildConfirmationCard(cardId, resolved);
await bot.say({
markdown:
`Review voice portal PIN reset for **Store ${resolved.storeNum}** ` +
`(\`${resolved.locationName || resolved.locationId}\`):`,
attachments: [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: adaptiveCard,
}],
});
} catch (err) {
logger('resetvppin', `Resolve failed for store ${raw}: ${err.message}`, 'warn');
await bot.say('markdown', `${err.message}`);
}
}