fix(api): start in production with safe fallbacks instead of crash-looping (502)
A production API with incomplete config threw before listening, so the container restarted forever and every /api request through nginx was a 502. The problems are now printed as one loud warning and each gets a safe fallback: - JWT_SECRET / JWT_CUSTOMER_SECRET missing, weak or equal: a random key is generated once and kept in the Setting table (boot-secrets.ts), so tokens survive restarts. A strong value in the environment always wins. JwtModule now uses registerAsync so it reads the key after boot fills it. - OTP_MASTER_CODE: ignored in production unless TEST_SERVER_ALLOW_MASTER_CODE=yes. - CORS_ORIGINS: invalid entries dropped; empty allows every origin. Tests: boot-secrets.spec, production-checks.spec. 118 tests pass. سرور با تنظیمات ناقص دیگر مدام ریست نمیشود و ورود دوباره کار میکند. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { productionConfigProblems } from './production-checks';
|
||||
import { isValidOrigin, masterCodeAllowed, productionConfigProblems } from './production-checks';
|
||||
|
||||
describe('productionConfigProblems', () => {
|
||||
const A = 'a'.repeat(40);
|
||||
@@ -46,4 +46,17 @@ describe('productionConfigProblems', () => {
|
||||
it('lists every problem at once', () => {
|
||||
expect(productionConfigProblems({ NODE_ENV: 'production', OTP_MASTER_CODE: '1234' })).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('allows the fixed test login only outside production or with the test-server flag', () => {
|
||||
expect(masterCodeAllowed({ NODE_ENV: 'development' })).toBe(true);
|
||||
expect(masterCodeAllowed({ NODE_ENV: 'production' })).toBe(false);
|
||||
expect(masterCodeAllowed({ NODE_ENV: 'production', TEST_SERVER_ALLOW_MASTER_CODE: 'yes' })).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts only full origins', () => {
|
||||
expect(isValidOrigin('https://bnk.example.ir')).toBe(true);
|
||||
expect(isValidOrigin('*')).toBe(false);
|
||||
expect(isValidOrigin('bnk.example.ir')).toBe(false);
|
||||
expect(isValidOrigin('https://bnk.example.ir/app')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* What a production server must have before it may start (REL-01, REL-03,
|
||||
* SEC-04). Each problem is one line, in Persian then English; main.ts refuses
|
||||
* to boot while the list is non-empty. Pure (takes the env), so it is tested
|
||||
* without starting the app.
|
||||
* What a production server should have (REL-01, REL-03, SEC-04). Each problem
|
||||
* is one line, in Persian then English. main.ts prints them as a warning and
|
||||
* applies a safe fallback for each instead of refusing to boot: a refused boot
|
||||
* crash-loops the container behind nginx and every request 502s. Pure (takes
|
||||
* the env), so it is tested without starting the app.
|
||||
*/
|
||||
export const DEV_JWT_SECRET = 'dev-only-insecure-secret';
|
||||
|
||||
@@ -26,6 +27,21 @@ export function parseCorsOrigins(raw: string | undefined): string[] {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function isValidOrigin(origin: string): boolean {
|
||||
return origin !== '*' && /^https?:\/\/[^/\s]+$/i.test(origin);
|
||||
}
|
||||
|
||||
/** Missing, the development default, or too short to be a real random key. */
|
||||
export function isWeakSecret(value: string | null | undefined): boolean {
|
||||
const v = value?.trim();
|
||||
return !v || v === DEV_JWT_SECRET || v.length < MIN_SECRET_LENGTH;
|
||||
}
|
||||
|
||||
/** The fixed test OTP only works outside production, or on a test server that opts in. */
|
||||
export function masterCodeAllowed(env: Env): boolean {
|
||||
return env.NODE_ENV !== 'production' || val(env, ALLOW_MASTER_CODE_FLAG) === 'yes';
|
||||
}
|
||||
|
||||
export function productionConfigProblems(env: Env): string[] {
|
||||
if (env.NODE_ENV !== 'production') return [];
|
||||
const problems: string[] = [];
|
||||
@@ -61,7 +77,7 @@ export function productionConfigProblems(env: Env): string[] {
|
||||
'CORS_ORIGINS تنظیم نشده است؛ نشانی پنل و اپها را با ویرگول جدا بنویسید. / CORS_ORIGINS is not set: list the panel and app origins, comma-separated.',
|
||||
);
|
||||
} else {
|
||||
const bad = origins.filter((o) => o === '*' || !/^https?:\/\/[^/\s]+$/i.test(o));
|
||||
const bad = origins.filter((o) => !isValidOrigin(o));
|
||||
if (bad.length) {
|
||||
problems.push(
|
||||
`CORS_ORIGINS فقط نشانی کامل میپذیرد، مثل https://adm.example.ir — نادرست: ${bad.join(', ')} / CORS_ORIGINS takes full origins only; invalid: ${bad.join(', ')}`,
|
||||
|
||||
Reference in New Issue
Block a user