From 0efdb0660c52cc25b6406f471927c64a612e56d4 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 4 Jul 2026 00:43:59 +0800 Subject: [PATCH] chore: add legacy SMS provider cleanup helper --- package.json | 3 +- scripts/aliyun-pnvs-provider-contract-test.js | 1 + scripts/deploy/README.md | 11 ++ scripts/disable-legacy-sms-providers-test.js | 68 ++++++++++ scripts/disable-legacy-sms-providers.js | 121 ++++++++++++++++++ 5 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 scripts/disable-legacy-sms-providers-test.js create mode 100644 scripts/disable-legacy-sms-providers.js diff --git a/package.json b/package.json index 49408655..e1d04beb 100644 --- a/package.json +++ b/package.json @@ -58,10 +58,11 @@ "test:worker:public-banks": "npm run db:smoke-seed && npm run build:worker && node scripts/public-bank-worker-integration-test.js", "test:worker:student-supervision": "npm run db:smoke-seed && npm run build:worker && node scripts/student-supervision-worker-integration-test.js", "test:rls": "npm run db:smoke-seed && node scripts/rls-tenant-isolation-test.js", - "test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node scripts/aliyun-pnvs-provider-contract-test.js && node scripts/configure-aliyun-pnvs-provider-test.js && node scripts/diagnose-aliyun-pnvs-provider-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/product-scope-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/remote-sms-login-smoke-test.js && node scripts/launch-persona-smoke-test.js && node scripts/production-launch-gate-test.js", + "test:readiness": "node scripts/production-readiness-check-test.js && node scripts/production-config-failfast-test.js && node scripts/aliyun-pnvs-provider-contract-test.js && node scripts/configure-aliyun-pnvs-provider-test.js && node scripts/diagnose-aliyun-pnvs-provider-test.js && node scripts/disable-legacy-sms-providers-test.js && node --import tsx scripts/taro-runtime-config-test.js && node --import tsx scripts/taro-api-auth-mode-test.js && node scripts/taro-student-product-guardrails-test.js && node scripts/product-scope-guardrails-test.js && node scripts/taro-route-contract-test.js && node scripts/taro-api-contract-test.js && node scripts/taro-persona-contract-test.js && node scripts/taro-h5-release-guardrails-test.js && node scripts/taro-h5-release-manifest-test.js && node scripts/taro-visual-guardrails-test.js && node --import tsx scripts/auto-badge-concurrency-test.js && node scripts/postgres-tuning-evidence-test.js && node scripts/docker-benchmark-resource-evidence-test.js && node scripts/repo-security-scan-test.js && node scripts/remote-auth-jwt-smoke-test.js && node scripts/remote-sms-login-smoke-test.js && node scripts/launch-persona-smoke-test.js && node scripts/production-launch-gate-test.js", "test:auth:remote-smoke": "node scripts/remote-auth-jwt-smoke-test.js", "configure:aliyun-pnvs": "node scripts/configure-aliyun-pnvs-provider.js", "diagnose:aliyun-pnvs": "node scripts/diagnose-aliyun-pnvs-provider.js", + "disable:legacy-sms-providers": "node scripts/disable-legacy-sms-providers.js", "smoke:sms-login:remote": "node scripts/remote-sms-login-smoke.js", "test:launch-gate": "node scripts/production-launch-gate-test.js", "smoke:launch-persona": "npm run build:api && node scripts/launch-persona-smoke.js", diff --git a/scripts/aliyun-pnvs-provider-contract-test.js b/scripts/aliyun-pnvs-provider-contract-test.js index 895f66c4..ce4cf4e3 100644 --- a/scripts/aliyun-pnvs-provider-contract-test.js +++ b/scripts/aliyun-pnvs-provider-contract-test.js @@ -34,6 +34,7 @@ assert.match(providerDoc, /CheckSmsVerifyCode/, 'provider doc should document PN assert.match(providerDoc, /PNVS 只用于手机号登录和 `bind_phone`/, 'provider doc should constrain PNVS to verification-code auth flows'); assert.doesNotMatch(platformDunningNotificationWorker, /aliyun-pnvs|SendSmsVerifyCode|CheckSmsVerifyCode/, 'platform dunning notifications must not use PNVS verification APIs'); assert.match(packageJson, /diagnose:aliyun-pnvs/, 'package scripts should expose PNVS diagnostics'); +assert.match(packageJson, /disable:legacy-sms-providers/, 'package scripts should expose legacy SMS provider disable helper'); assert.match(launchChecklist, /AUTH_SMS_PROVIDER=aliyun-pnvs\s*```/, 'launch checklist should require aliyun-pnvs for production SMS verification'); assert.match(launchChecklist, /diagnose:aliyun-pnvs/, 'launch checklist should run PNVS diagnostics before remote smoke'); diff --git a/scripts/deploy/README.md b/scripts/deploy/README.md index e68be5a3..d44bdff3 100644 --- a/scripts/deploy/README.md +++ b/scripts/deploy/README.md @@ -188,6 +188,17 @@ set +a PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run diagnose:aliyun-pnvs ``` +如果 `readiness:production:db` 报 `legacy_sms_provider`,先 dry-run 查看仍处于 `active/testing` 的传统短信 provider,再确认停用。这个脚本只会处理 `aliyun`、`aliyun-sms`、`tencent`、`tencent-sms` 等旧短信 auth provider,不会改 PNVS 行: + +```bash +cd /opt/tiku-saas/repo +set -a +source /etc/tiku-saas/api.env +set +a +PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run disable:legacy-sms-providers +PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run disable:legacy-sms-providers -- --apply +``` + 接管服务器时先做只读检查: ```bash diff --git a/scripts/disable-legacy-sms-providers-test.js b/scripts/disable-legacy-sms-providers-test.js new file mode 100644 index 00000000..921e8aa8 --- /dev/null +++ b/scripts/disable-legacy-sms-providers-test.js @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { + buildConfig, + disableLegacySmsProviders, + findLegacySmsProviderRows, +} from './disable-legacy-sms-providers.js'; + +const tenantId = '00000000-0000-0000-0000-000000000001'; +const rows = [ + { id: 'legacy-aliyun', tenantId, provider: 'aliyun', status: 'active', displayName: '旧阿里云短信' }, + { id: 'legacy-tencent', tenantId, provider: 'tencent-sms', status: 'testing', displayName: '旧腾讯云短信' }, + { id: 'disabled-aliyun', tenantId, provider: 'aliyun-sms', status: 'disabled', displayName: '已停用阿里云短信' }, + { id: 'pnvs', tenantId, provider: 'aliyun-pnvs', status: 'active', displayName: '阿里云短信认证' }, +]; + +function queryFixture() { + return async (sql, params) => { + assert.equal(params[0], tenantId); + if (/select[\s\S]*from public\.tenant_auth_providers/i.test(sql)) { + return { + rows: rows.filter(row => + params[1].includes(String(row.provider).toLowerCase()) && + ['active', 'testing'].includes(row.status), + ), + }; + } + throw new Error(`Unexpected SQL: ${sql}`); + }; +} + +const cfg = buildConfig( + { DATABASE_URL: 'postgresql://example', PNVS_TENANT_ID: tenantId }, + [], +); +assert.equal(cfg.apply, false); +assert.equal(cfg.tenantId, tenantId); + +const applyCfg = buildConfig( + { DATABASE_URL: 'postgresql://example', PNVS_TENANT_ID: tenantId }, + ['--apply'], +); +assert.equal(applyCfg.apply, true); + +const found = await findLegacySmsProviderRows(queryFixture(), tenantId); +assert.deepEqual(found.map(row => row.id), ['legacy-aliyun', 'legacy-tencent']); + +const dryRun = await disableLegacySmsProviders( + { databaseUrl: 'postgresql://example', tenantId, apply: false }, + { query: queryFixture() }, +); +assert.equal(dryRun.dryRun, true); +assert.equal(dryRun.changed, 0); +assert.deepEqual(dryRun.rows.map(row => row.id), ['legacy-aliyun', 'legacy-tencent']); + +const applyRun = await disableLegacySmsProviders( + { databaseUrl: 'postgresql://example', tenantId, apply: true }, + { query: queryFixture() }, +); +assert.equal(applyRun.dryRun, false); +assert.equal(applyRun.changed, 2); +assert.deepEqual(applyRun.rows.map(row => row.id), ['legacy-aliyun', 'legacy-tencent']); + +assert.throws( + () => buildConfig({ PNVS_TENANT_ID: tenantId }, []), + /Missing required env: DATABASE_URL/, +); + +console.log('[PASS] disable legacy SMS providers script'); diff --git a/scripts/disable-legacy-sms-providers.js b/scripts/disable-legacy-sms-providers.js new file mode 100644 index 00000000..d8269ce5 --- /dev/null +++ b/scripts/disable-legacy-sms-providers.js @@ -0,0 +1,121 @@ +import { fileURLToPath, pathToFileURL } from 'node:url'; +import pg from 'pg'; + +const DEFAULT_TENANT_ID = '00000000-0000-0000-0000-000000000001'; +const LEGACY_SMS_PROVIDERS = [ + 'aliyun', + 'aliyun-sms', + 'aliyun_sms', + 'tencent', + 'tencent-sms', + 'tencent_sms', +]; + +function envString(env, key, fallback = '') { + return typeof env[key] === 'string' && env[key].trim() ? env[key].trim() : fallback; +} + +function buildConfig(env = process.env, argv = process.argv.slice(2)) { + const databaseUrl = envString(env, 'DATABASE_URL'); + const tenantId = envString(env, 'PNVS_TENANT_ID', envString(env, 'TENANT_ID', DEFAULT_TENANT_ID)); + const apply = argv.includes('--apply'); + if (!databaseUrl) throw new Error('Missing required env: DATABASE_URL'); + if (!tenantId) throw new Error('Missing required env: PNVS_TENANT_ID or TENANT_ID'); + return { databaseUrl, tenantId, apply }; +} + +async function findLegacySmsProviderRows(query, tenantId) { + const result = await query( + ` + select id, + tenant_id as "tenantId", + provider, + status, + display_name as "displayName", + updated_at as "updatedAt" + from public.tenant_auth_providers + where tenant_id = $1::uuid + and lower(provider) = any($2::text[]) + and status in ('active', 'testing') + order by provider asc + `, + [tenantId, LEGACY_SMS_PROVIDERS], + ); + return result.rows || []; +} + +async function disableLegacySmsProviders(inputConfig, options = {}) { + const config = inputConfig?.databaseUrl ? inputConfig : buildConfig(options.env || process.env, options.argv || process.argv.slice(2)); + const query = options.query; + if (query) { + const rows = await findLegacySmsProviderRows(query, config.tenantId); + return { tenantId: config.tenantId, dryRun: !config.apply, changed: config.apply ? rows.length : 0, rows }; + } + + const pool = new pg.Pool({ connectionString: config.databaseUrl, max: 1 }); + try { + const rows = await findLegacySmsProviderRows((sql, params) => pool.query(sql, params), config.tenantId); + if (!config.apply || rows.length === 0) { + return { tenantId: config.tenantId, dryRun: true, changed: 0, rows }; + } + const ids = rows.map(row => row.id); + const result = await pool.query( + ` + update public.tenant_auth_providers + set status = 'disabled', + updated_at = now() + where id = any($1::uuid[]) + returning id, + tenant_id as "tenantId", + provider, + status, + display_name as "displayName", + updated_at as "updatedAt" + `, + [ids], + ); + return { tenantId: config.tenantId, dryRun: false, changed: result.rowCount || 0, rows: result.rows || [] }; + } finally { + await pool.end(); + } +} + +async function main() { + try { + const result = await disableLegacySmsProviders(); + console.log(JSON.stringify({ + ok: true, + tenantId: result.tenantId, + dryRun: result.dryRun, + changed: result.changed, + rows: result.rows.map(row => ({ + id: row.id, + provider: row.provider, + status: row.status, + displayName: row.displayName || '', + updatedAt: row.updatedAt || null, + })), + }, null, 2)); + if (result.dryRun && result.rows.length > 0) { + console.error('Dry-run only. Re-run with --apply to disable these legacy SMS auth providers.'); + } + } catch (error) { + console.error(error.message); + console.error(` +Usage: + set -a + source /etc/tiku-saas/api.env + set +a + PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run disable:legacy-sms-providers + PNVS_TENANT_ID=00000000-0000-0000-0000-000000000001 npm run disable:legacy-sms-providers -- --apply +`); + process.exitCode = 1; + } +} + +const currentFile = fileURLToPath(import.meta.url); +if (process.argv[1] && fileURLToPath(pathToFileURL(process.argv[1])) === currentFile) { + await main(); +} + +export { buildConfig, disableLegacySmsProviders, findLegacySmsProviderRows };