forked from wangziqi/gongxue-base
chore: add legacy SMS provider cleanup helper
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
68
scripts/disable-legacy-sms-providers-test.js
Normal file
68
scripts/disable-legacy-sms-providers-test.js
Normal file
@@ -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');
|
||||
121
scripts/disable-legacy-sms-providers.js
Normal file
121
scripts/disable-legacy-sms-providers.js
Normal file
@@ -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 };
|
||||
Reference in New Issue
Block a user