forked from wangziqi/gongxue-base
feat: support Aliyun PNVS SMS verification
This commit is contained in:
26
scripts/aliyun-pnvs-provider-contract-test.js
Normal file
26
scripts/aliyun-pnvs-provider-contract-test.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const providerSource = fs.readFileSync(path.join(process.cwd(), 'apps/api/src/features/auth/providers.ts'), 'utf8');
|
||||
const routeSource = fs.readFileSync(path.join(process.cwd(), 'apps/api/src/features/auth/routes.ts'), 'utf8');
|
||||
const deployEnvExample = fs.readFileSync(path.join(process.cwd(), 'scripts/deploy/env/api.env.example'), 'utf8');
|
||||
const providerDoc = fs.readFileSync(path.join(process.cwd(), 'docs/refactor/auth-payment-provider-plan.md'), 'utf8');
|
||||
const launchChecklist = fs.readFileSync(path.join(process.cwd(), 'docs/refactor/web-launch-acceptance-checklist.md'), 'utf8');
|
||||
const readinessSource = fs.readFileSync(path.join(process.cwd(), 'scripts/production-readiness-check.js'), 'utf8');
|
||||
|
||||
assert.match(providerSource, /class AliyunPnvsSmsProvider/, 'PNVS provider class should exist');
|
||||
assert.match(providerSource, /SendSmsVerifyCode/, 'PNVS provider should call SendSmsVerifyCode');
|
||||
assert.match(providerSource, /CheckSmsVerifyCode/, 'PNVS provider should call CheckSmsVerifyCode');
|
||||
assert.match(providerSource, /VerifyResult.*PASS/s, 'PNVS verify should require provider PASS result');
|
||||
assert.match(providerSource, /##code##/, 'PNVS provider should preserve Aliyun-generated code placeholder');
|
||||
assert.match(routeSource, /aliyun-pnvs/, 'auth routes should recognize aliyun-pnvs aliases');
|
||||
assert.match(routeSource, /verification.*provider/s, 'auth routes should store provider verification mode');
|
||||
assert.match(routeSource, /provider\.verify/, 'auth routes should delegate PNVS verification to provider');
|
||||
assert.match(deployEnvExample, /AUTH_SMS_PROVIDER=aliyun-pnvs/, 'deploy env example should prefer aliyun-pnvs');
|
||||
assert.match(readinessSource, /aliyunPnvs/, 'production readiness should validate aliyun-pnvs public config');
|
||||
assert.match(providerDoc, /SendSmsVerifyCode/, 'provider doc should document PNVS send action');
|
||||
assert.match(providerDoc, /CheckSmsVerifyCode/, 'provider doc should document PNVS verify action');
|
||||
assert.match(launchChecklist, /AUTH_SMS_PROVIDER=aliyun-pnvs/, 'launch checklist should allow aliyun-pnvs');
|
||||
|
||||
console.log('[PASS] Aliyun PNVS provider contract');
|
||||
8
scripts/deploy/env/api.env.example
vendored
8
scripts/deploy/env/api.env.example
vendored
@@ -18,11 +18,9 @@ AUTH_SESSION_SECRET=replace-with-strong-random-session-secret
|
||||
AUTH_CODE_PEPPER=replace-with-strong-random-code-pepper
|
||||
PLATFORM_ADMIN_API_KEY=replace-with-strong-random-platform-admin-key
|
||||
|
||||
AUTH_SMS_PROVIDER=aliyun
|
||||
ALIYUN_SMS_ACCESS_KEY_ID=replace-with-access-key-id
|
||||
ALIYUN_SMS_ACCESS_KEY_SECRET=replace-with-access-key-secret
|
||||
ALIYUN_SMS_SIGN_NAME=replace-with-sms-sign
|
||||
ALIYUN_SMS_TEMPLATE_LOGIN=replace-with-template-code
|
||||
# Supported production values: aliyun, aliyun-pnvs, tencent.
|
||||
# Tenant-level SMS AccessKey/SecretKey live in app_private.tenant_secrets, not in this env file.
|
||||
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||
|
||||
WECHAT_MINIAPP_APP_ID=replace-with-miniapp-app-id
|
||||
WECHAT_MINIAPP_APP_SECRET=replace-with-miniapp-app-secret
|
||||
|
||||
@@ -69,7 +69,7 @@ const unsafeApiSmsProvider = runImport(apiConfigUrl, {
|
||||
assert.notEqual(unsafeApiSmsProvider.status, 0, 'production API config should reject unsupported SMS provider');
|
||||
assert.match(
|
||||
unsafeApiSmsProvider.output,
|
||||
/AUTH_SMS_PROVIDER must be aliyun\/aliyun-sms or tencent\/tencent-sms/,
|
||||
/AUTH_SMS_PROVIDER must be aliyun\/aliyun-sms, aliyun-pnvs, or tencent\/tencent-sms/,
|
||||
'API config should name supported production SMS providers',
|
||||
);
|
||||
|
||||
|
||||
@@ -132,6 +132,39 @@ assert.ok(
|
||||
'env-only readiness should explicitly warn that DB checks are skipped',
|
||||
);
|
||||
|
||||
const safeAliyunPnvs = runReadiness(`
|
||||
NODE_ENV=production
|
||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
||||
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
||||
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||
AUTH_CODE_PEPPER=${strongSecretA}
|
||||
AUTH_SESSION_SECRET=${strongSecretB}
|
||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
||||
ALLOW_LEGACY_AUTH_HEADERS=false
|
||||
ALLOW_PLATFORM_ADMIN_KEY=false
|
||||
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
||||
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
||||
STORAGE_DEFAULT_BUCKET=tiku-assets
|
||||
STORAGE_REQUIRE_TENANT_PREFIX=true
|
||||
ALIYUN_OSS_REGION=cn-hangzhou
|
||||
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
||||
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
||||
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
||||
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
||||
WORKER_CRM_BATCH_SIZE=20
|
||||
WORKER_COMMERCE_BATCH_SIZE=20
|
||||
WORKER_ASSET_BATCH_SIZE=50
|
||||
WORKER_IMPORT_BATCH_SIZE=5
|
||||
WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE=5
|
||||
`);
|
||||
|
||||
assert.equal(safeAliyunPnvs.status, 0, `aliyun-pnvs readiness should pass without blockers: ${safeAliyunPnvs.stdout} ${safeAliyunPnvs.stderr}`);
|
||||
assert.equal(safeAliyunPnvs.payload.summary?.blocker, 0, 'aliyun-pnvs readiness should have no blockers');
|
||||
|
||||
const unsafeProviderFixture = runReadiness(
|
||||
`
|
||||
NODE_ENV=production
|
||||
|
||||
@@ -10,11 +10,23 @@ const DEFAULT_AUTH_SESSION_SECRET = 'development-session-secret-change-me';
|
||||
const DEFAULT_AUTH_JWT_SECRET = 'development-jwt-secret-change-me';
|
||||
const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key';
|
||||
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
||||
const PRODUCTION_SMS_PROVIDERS = new Set(['aliyun', 'aliyun-sms', 'aliyun_sms', 'tencent', 'tencent-sms', 'tencent_sms']);
|
||||
const PRODUCTION_SMS_PROVIDERS = new Set([
|
||||
'aliyun',
|
||||
'aliyun-sms',
|
||||
'aliyun_sms',
|
||||
'aliyun-pnvs',
|
||||
'aliyun_pnvs',
|
||||
'aliyun-sms-auth',
|
||||
'aliyun_sms_auth',
|
||||
'tencent',
|
||||
'tencent-sms',
|
||||
'tencent_sms',
|
||||
]);
|
||||
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
||||
const AUTH_PROVIDER_ALIASES = {
|
||||
sms: new Set(['aliyun', 'aliyun-sms', 'aliyun_sms', 'tencent', 'tencent-sms', 'tencent_sms']),
|
||||
sms: new Set(['aliyun', 'aliyun-sms', 'aliyun_sms', 'aliyun-pnvs', 'aliyun_pnvs', 'aliyun-sms-auth', 'aliyun_sms_auth', 'tencent', 'tencent-sms', 'tencent_sms']),
|
||||
aliyun: new Set(['aliyun', 'aliyun-sms', 'aliyun_sms']),
|
||||
aliyunPnvs: new Set(['aliyun-pnvs', 'aliyun_pnvs', 'aliyun-pnvs-sms', 'aliyun_sms_auth', 'aliyun-sms-auth']),
|
||||
tencent: new Set(['tencent', 'tencent-sms', 'tencent_sms']),
|
||||
wechatMiniapp: new Set(['wechat-miniapp', 'wechat_miniapp', 'wechat-mini', 'wx-miniapp', 'wx_miniapp']),
|
||||
wechatWeb: new Set(['wechat-web', 'wechat_web', 'wechat', 'wechat-oauth', 'wechat_oauth']),
|
||||
@@ -232,6 +244,23 @@ function validateAuthProviderPublicConfig(row) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerIn(provider, AUTH_PROVIDER_ALIASES.aliyunPnvs)) {
|
||||
blockMissingPublicConfig(row, missingPublicKeyGroups(configPublic, [
|
||||
{ label: 'signName', keys: ['signName'] },
|
||||
{ label: 'templateCode', keys: ['templateCode'] },
|
||||
]));
|
||||
validateProviderUrl({
|
||||
id: `db.auth.${safeProviderName(row.provider)}.endpoint`,
|
||||
value: publicString(configPublic, ['endpoint']),
|
||||
allowedHosts: ['aliyuncs.com'],
|
||||
details,
|
||||
});
|
||||
if (!publicString(configPublic, ['regionId'])) {
|
||||
warn(`db.auth.${safeProviderName(row.provider)}.region`, 'Aliyun PNVS regionId is not set; default cn-hangzhou will be used', details);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (providerIn(provider, AUTH_PROVIDER_ALIASES.tencent)) {
|
||||
blockMissingPublicConfig(row, missingPublicKeyGroups(configPublic, [
|
||||
{ label: 'smsSdkAppId/appId', keys: ['smsSdkAppId', 'appId'] },
|
||||
@@ -507,7 +536,7 @@ function validateEnv() {
|
||||
|
||||
const authSmsProvider = env('AUTH_SMS_PROVIDER', 'mock').trim().toLowerCase();
|
||||
if (!PRODUCTION_SMS_PROVIDERS.has(authSmsProvider)) {
|
||||
block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun/aliyun-sms or tencent/tencent-sms in production', {
|
||||
block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun/aliyun-sms, aliyun-pnvs, or tencent/tencent-sms in production', {
|
||||
provider: authSmsProvider || '(empty)',
|
||||
});
|
||||
} else {
|
||||
@@ -842,7 +871,7 @@ async function validateDatabase() {
|
||||
left join app_private.tenant_secrets s
|
||||
on s.tenant_id = p.tenant_id
|
||||
and s.secret_scope = case
|
||||
when lower(replace(p.provider, '_', '-')) in ('aliyun', 'aliyun-sms', 'tencent', 'tencent-sms') then 'sms'
|
||||
when lower(replace(p.provider, '_', '-')) in ('aliyun', 'aliyun-sms', 'aliyun-pnvs', 'aliyun-pnvs-sms', 'aliyun-sms-auth', 'tencent', 'tencent-sms') then 'sms'
|
||||
else 'oauth'
|
||||
end
|
||||
and s.secret_key = coalesce(nullif(split_part(p.config_public->>'secretRef', ':', 3), ''), p.provider)
|
||||
|
||||
Reference in New Issue
Block a user