forked from wangziqi/gongxue-base
fix: require PNVS SMS provider in production
This commit is contained in:
@@ -54,16 +54,11 @@ const DEFAULT_MAX_JSON_BODY_BYTES = 1024 * 1024;
|
|||||||
const DEFAULT_MAX_IMPORT_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
const DEFAULT_MAX_IMPORT_JSON_BODY_BYTES = 10 * 1024 * 1024;
|
||||||
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
||||||
const PRODUCTION_SMS_PROVIDERS = new Set([
|
const PRODUCTION_SMS_PROVIDERS = new Set([
|
||||||
'aliyun',
|
|
||||||
'aliyun-sms',
|
|
||||||
'aliyun_sms',
|
|
||||||
'aliyun-pnvs',
|
'aliyun-pnvs',
|
||||||
'aliyun_pnvs',
|
'aliyun_pnvs',
|
||||||
|
'aliyun-pnvs-sms',
|
||||||
'aliyun-sms-auth',
|
'aliyun-sms-auth',
|
||||||
'aliyun_sms_auth',
|
'aliyun_sms_auth',
|
||||||
'tencent',
|
|
||||||
'tencent-sms',
|
|
||||||
'tencent_sms',
|
|
||||||
]);
|
]);
|
||||||
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
||||||
|
|
||||||
@@ -113,7 +108,7 @@ function validateProductionConfig(nextConfig: ApiConfig) {
|
|||||||
const failures: string[] = [];
|
const failures: string[] = [];
|
||||||
if (nextConfig.corsOrigins.includes('*')) failures.push('CORS_ORIGIN must not include * in production');
|
if (nextConfig.corsOrigins.includes('*')) failures.push('CORS_ORIGIN must not include * in production');
|
||||||
if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) {
|
if (!PRODUCTION_SMS_PROVIDERS.has(nextConfig.authSmsProvider.trim().toLowerCase())) {
|
||||||
failures.push('AUTH_SMS_PROVIDER must be aliyun/aliyun-sms, aliyun-pnvs, or tencent/tencent-sms in production');
|
failures.push('AUTH_SMS_PROVIDER must be aliyun-pnvs in production');
|
||||||
}
|
}
|
||||||
if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) {
|
if (isUnsafeSecret(nextConfig.authCodePepper, DEFAULT_AUTH_CODE_PEPPER)) {
|
||||||
failures.push('AUTH_CODE_PEPPER must be a strong production secret');
|
failures.push('AUTH_CODE_PEPPER must be a strong production secret');
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const safeBaseEnv = {
|
|||||||
const safeApiEnv = {
|
const safeApiEnv = {
|
||||||
...safeBaseEnv,
|
...safeBaseEnv,
|
||||||
CORS_ORIGIN: 'https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com',
|
CORS_ORIGIN: 'https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com',
|
||||||
AUTH_SMS_PROVIDER: 'aliyun',
|
AUTH_SMS_PROVIDER: 'aliyun-pnvs',
|
||||||
AUTH_CODE_PEPPER: 's3cure-prod-code-pepper-2026-06-30-abcdef',
|
AUTH_CODE_PEPPER: 's3cure-prod-code-pepper-2026-06-30-abcdef',
|
||||||
AUTH_SESSION_SECRET: 's3cure-prod-session-secret-2026-06-30-ghijkl',
|
AUTH_SESSION_SECRET: 's3cure-prod-session-secret-2026-06-30-ghijkl',
|
||||||
AUTH_JWT_JWKS_URL: 'https://auth.gongxue100.com/auth/v1/.well-known/jwks.json',
|
AUTH_JWT_JWKS_URL: 'https://auth.gongxue100.com/auth/v1/.well-known/jwks.json',
|
||||||
@@ -69,8 +69,19 @@ const unsafeApiSmsProvider = runImport(apiConfigUrl, {
|
|||||||
assert.notEqual(unsafeApiSmsProvider.status, 0, 'production API config should reject unsupported SMS provider');
|
assert.notEqual(unsafeApiSmsProvider.status, 0, 'production API config should reject unsupported SMS provider');
|
||||||
assert.match(
|
assert.match(
|
||||||
unsafeApiSmsProvider.output,
|
unsafeApiSmsProvider.output,
|
||||||
/AUTH_SMS_PROVIDER must be aliyun\/aliyun-sms, aliyun-pnvs, or tencent\/tencent-sms/,
|
/AUTH_SMS_PROVIDER must be aliyun-pnvs in production/,
|
||||||
'API config should name supported production SMS providers',
|
'API config should require PNVS for production SMS',
|
||||||
|
);
|
||||||
|
|
||||||
|
const unsafeApiTraditionalSmsProvider = runImport(apiConfigUrl, {
|
||||||
|
...safeApiEnv,
|
||||||
|
AUTH_SMS_PROVIDER: 'aliyun',
|
||||||
|
});
|
||||||
|
assert.notEqual(unsafeApiTraditionalSmsProvider.status, 0, 'production API config should reject traditional Aliyun SMS provider');
|
||||||
|
assert.match(
|
||||||
|
unsafeApiTraditionalSmsProvider.output,
|
||||||
|
/AUTH_SMS_PROVIDER must be aliyun-pnvs in production/,
|
||||||
|
'API config should reject non-PNVS SMS providers in production',
|
||||||
);
|
);
|
||||||
|
|
||||||
const unsafeApiStoragePublicBaseUrl = runImport(apiConfigUrl, {
|
const unsafeApiStoragePublicBaseUrl = runImport(apiConfigUrl, {
|
||||||
|
|||||||
@@ -84,6 +84,37 @@ assert.ok(
|
|||||||
'readiness should block unsupported AUTH_SMS_PROVIDER values',
|
'readiness should block unsupported AUTH_SMS_PROVIDER values',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const traditionalAliyunSmsProvider = runReadiness(`
|
||||||
|
NODE_ENV=production
|
||||||
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
||||||
|
CORS_ORIGIN=https://student.gongxue100.com
|
||||||
|
AUTH_SMS_PROVIDER=aliyun
|
||||||
|
AUTH_CODE_PEPPER=s3cure-prod-code-pepper-2026-06-29-abcdef
|
||||||
|
AUTH_SESSION_SECRET=s3cure-prod-session-secret-2026-06-29-ghijkl
|
||||||
|
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=s3cure-platform-admin-key-2026-06-29-mnopqr
|
||||||
|
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
|
||||||
|
`);
|
||||||
|
|
||||||
|
assert.notEqual(traditionalAliyunSmsProvider.status, 0, 'traditional Aliyun SMS provider readiness should fail in production');
|
||||||
|
assert.ok(
|
||||||
|
traditionalAliyunSmsProvider.payload.checks?.some(item => item.id === 'env.auth_sms_provider' && item.status === 'blocker'),
|
||||||
|
'readiness should require PNVS instead of traditional Aliyun SMS',
|
||||||
|
);
|
||||||
|
|
||||||
const strongSecretA = 's3cure-prod-code-pepper-2026-06-29-abcdef';
|
const strongSecretA = 's3cure-prod-code-pepper-2026-06-29-abcdef';
|
||||||
const strongSecretB = 's3cure-prod-session-secret-2026-06-29-ghijkl';
|
const strongSecretB = 's3cure-prod-session-secret-2026-06-29-ghijkl';
|
||||||
const strongSecretC = 's3cure-platform-admin-key-2026-06-29-mnopqr';
|
const strongSecretC = 's3cure-platform-admin-key-2026-06-29-mnopqr';
|
||||||
@@ -92,7 +123,7 @@ const safe = runReadiness(`
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
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
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
||||||
AUTH_SMS_PROVIDER=aliyun
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||||
AUTH_CODE_PEPPER=${strongSecretA}
|
AUTH_CODE_PEPPER=${strongSecretA}
|
||||||
AUTH_SESSION_SECRET=${strongSecretB}
|
AUTH_SESSION_SECRET=${strongSecretB}
|
||||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||||
@@ -269,7 +300,7 @@ const unsafeProviderFixture = runReadiness(
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
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
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
||||||
AUTH_SMS_PROVIDER=aliyun
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||||
AUTH_CODE_PEPPER=${strongSecretA}
|
AUTH_CODE_PEPPER=${strongSecretA}
|
||||||
AUTH_SESSION_SECRET=${strongSecretB}
|
AUTH_SESSION_SECRET=${strongSecretB}
|
||||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||||
@@ -346,7 +377,7 @@ const missingJwksIssuer = runReadiness(`
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
||||||
CORS_ORIGIN=https://student.gongxue100.com
|
CORS_ORIGIN=https://student.gongxue100.com
|
||||||
AUTH_SMS_PROVIDER=aliyun
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||||
AUTH_CODE_PEPPER=${strongSecretA}
|
AUTH_CODE_PEPPER=${strongSecretA}
|
||||||
AUTH_SESSION_SECRET=${strongSecretB}
|
AUTH_SESSION_SECRET=${strongSecretB}
|
||||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||||
@@ -378,7 +409,7 @@ const unsafePlatformAuditNotificationLocalhost = runReadiness(`
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
||||||
CORS_ORIGIN=https://student.gongxue100.com
|
CORS_ORIGIN=https://student.gongxue100.com
|
||||||
AUTH_SMS_PROVIDER=aliyun
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||||
AUTH_CODE_PEPPER=${strongSecretA}
|
AUTH_CODE_PEPPER=${strongSecretA}
|
||||||
AUTH_SESSION_SECRET=${strongSecretB}
|
AUTH_SESSION_SECRET=${strongSecretB}
|
||||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||||
@@ -410,7 +441,7 @@ const unsafePlatformDunningNotificationLocalhost = runReadiness(`
|
|||||||
NODE_ENV=production
|
NODE_ENV=production
|
||||||
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
||||||
CORS_ORIGIN=https://student.gongxue100.com
|
CORS_ORIGIN=https://student.gongxue100.com
|
||||||
AUTH_SMS_PROVIDER=aliyun
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
||||||
AUTH_CODE_PEPPER=${strongSecretA}
|
AUTH_CODE_PEPPER=${strongSecretA}
|
||||||
AUTH_SESSION_SECRET=${strongSecretB}
|
AUTH_SESSION_SECRET=${strongSecretB}
|
||||||
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
||||||
|
|||||||
@@ -11,16 +11,11 @@ const DEFAULT_AUTH_JWT_SECRET = 'development-jwt-secret-change-me';
|
|||||||
const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key';
|
const DEFAULT_PLATFORM_ADMIN_API_KEY = 'local-platform-admin-key';
|
||||||
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
const HARD_MAX_JSON_BODY_BYTES = 50 * 1024 * 1024;
|
||||||
const PRODUCTION_SMS_PROVIDERS = new Set([
|
const PRODUCTION_SMS_PROVIDERS = new Set([
|
||||||
'aliyun',
|
|
||||||
'aliyun-sms',
|
|
||||||
'aliyun_sms',
|
|
||||||
'aliyun-pnvs',
|
'aliyun-pnvs',
|
||||||
'aliyun_pnvs',
|
'aliyun_pnvs',
|
||||||
|
'aliyun-pnvs-sms',
|
||||||
'aliyun-sms-auth',
|
'aliyun-sms-auth',
|
||||||
'aliyun_sms_auth',
|
'aliyun_sms_auth',
|
||||||
'tencent',
|
|
||||||
'tencent-sms',
|
|
||||||
'tencent_sms',
|
|
||||||
]);
|
]);
|
||||||
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
const PRODUCTION_STORAGE_PROVIDERS = new Set(['aliyun_oss', 'tencent_cos', 'supabase_storage']);
|
||||||
const AUTH_PROVIDER_ALIASES = {
|
const AUTH_PROVIDER_ALIASES = {
|
||||||
@@ -174,8 +169,6 @@ function providerIn(provider, aliases) {
|
|||||||
function currentSmsProviderAliases() {
|
function currentSmsProviderAliases() {
|
||||||
const provider = normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')).replace(/_/g, '-');
|
const provider = normalizeProvider(env('AUTH_SMS_PROVIDER', 'mock')).replace(/_/g, '-');
|
||||||
if (AUTH_PROVIDER_ALIASES.aliyunPnvs.has(provider)) return { provider: 'aliyun-pnvs', aliases: AUTH_PROVIDER_ALIASES.aliyunPnvs };
|
if (AUTH_PROVIDER_ALIASES.aliyunPnvs.has(provider)) return { provider: 'aliyun-pnvs', aliases: AUTH_PROVIDER_ALIASES.aliyunPnvs };
|
||||||
if (AUTH_PROVIDER_ALIASES.aliyun.has(provider)) return { provider: 'aliyun', aliases: AUTH_PROVIDER_ALIASES.aliyun };
|
|
||||||
if (AUTH_PROVIDER_ALIASES.tencent.has(provider)) return { provider: 'tencent', aliases: AUTH_PROVIDER_ALIASES.tencent };
|
|
||||||
return { provider, aliases: new Set([provider]) };
|
return { provider, aliases: new Set([provider]) };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,11 +569,11 @@ function validateEnv() {
|
|||||||
|
|
||||||
const authSmsProvider = env('AUTH_SMS_PROVIDER', 'mock').trim().toLowerCase();
|
const authSmsProvider = env('AUTH_SMS_PROVIDER', 'mock').trim().toLowerCase();
|
||||||
if (!PRODUCTION_SMS_PROVIDERS.has(authSmsProvider)) {
|
if (!PRODUCTION_SMS_PROVIDERS.has(authSmsProvider)) {
|
||||||
block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun/aliyun-sms, aliyun-pnvs, or tencent/tencent-sms in production', {
|
block('env.auth_sms_provider', 'AUTH_SMS_PROVIDER must be aliyun-pnvs in production', {
|
||||||
provider: authSmsProvider || '(empty)',
|
provider: authSmsProvider || '(empty)',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
pass('env.auth_sms_provider', 'AUTH_SMS_PROVIDER is a supported production SMS provider', { provider: authSmsProvider });
|
pass('env.auth_sms_provider', 'AUTH_SMS_PROVIDER is aliyun-pnvs for production SMS authentication', { provider: authSmsProvider });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isUnsafeSecret(env('AUTH_CODE_PEPPER', DEFAULT_AUTH_CODE_PEPPER), DEFAULT_AUTH_CODE_PEPPER)) {
|
if (isUnsafeSecret(env('AUTH_CODE_PEPPER', DEFAULT_AUTH_CODE_PEPPER), DEFAULT_AUTH_CODE_PEPPER)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user