1
This commit is contained in:
@@ -7,9 +7,12 @@ export type AppConfig = {
|
||||
publicDir: string;
|
||||
logsDir: string;
|
||||
dataDir: string;
|
||||
sqlitePath: string;
|
||||
rawEnv: Record<string, string>;
|
||||
databaseUrl: string;
|
||||
serverAddr: string;
|
||||
logLevel: 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
||||
editorApiEnabled: boolean;
|
||||
assetsApiEnabled: boolean;
|
||||
jwtSecret: string;
|
||||
jwtExpiresIn: string;
|
||||
jwtIssuer: string;
|
||||
@@ -24,6 +27,59 @@ export type AppConfig = {
|
||||
imageModel: string;
|
||||
requestTimeoutMs: number;
|
||||
};
|
||||
smsAuth: {
|
||||
enabled: boolean;
|
||||
provider: 'aliyun' | 'mock';
|
||||
endpoint: string;
|
||||
accessKeyId: string;
|
||||
accessKeySecret: string;
|
||||
signName: string;
|
||||
templateCode: string;
|
||||
templateParamKey: string;
|
||||
countryCode: string;
|
||||
schemeName: string;
|
||||
codeLength: number;
|
||||
codeType: number;
|
||||
validTimeSeconds: number;
|
||||
intervalSeconds: number;
|
||||
duplicatePolicy: number;
|
||||
caseAuthPolicy: number;
|
||||
returnVerifyCode: boolean;
|
||||
mockVerifyCode: string;
|
||||
maxSendPerPhonePerDay: number;
|
||||
maxSendPerIpPerHour: number;
|
||||
maxVerifyFailuresPerPhonePerHour: number;
|
||||
maxVerifyFailuresPerIpPerHour: number;
|
||||
captchaTtlSeconds: number;
|
||||
captchaTriggerVerifyFailuresPerPhone: number;
|
||||
captchaTriggerVerifyFailuresPerIp: number;
|
||||
blockPhoneFailureThreshold: number;
|
||||
blockIpFailureThreshold: number;
|
||||
blockPhoneDurationMinutes: number;
|
||||
blockIpDurationMinutes: number;
|
||||
};
|
||||
wechatAuth: {
|
||||
enabled: boolean;
|
||||
provider: 'wechat' | 'mock';
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
authorizeEndpoint: string;
|
||||
accessTokenEndpoint: string;
|
||||
userInfoEndpoint: string;
|
||||
callbackPath: string;
|
||||
defaultRedirectPath: string;
|
||||
mockUserId: string;
|
||||
mockUnionId: string;
|
||||
mockDisplayName: string;
|
||||
mockAvatarUrl: string;
|
||||
};
|
||||
authSession: {
|
||||
refreshCookieName: string;
|
||||
refreshSessionTtlDays: number;
|
||||
refreshCookieSecure: boolean;
|
||||
refreshCookieSameSite: 'Lax' | 'Strict' | 'None';
|
||||
refreshCookiePath: string;
|
||||
};
|
||||
};
|
||||
|
||||
type LoadConfigOptions = {
|
||||
@@ -101,11 +157,80 @@ function readPositiveInt(
|
||||
return Number.isFinite(parsed) && parsed > 0 ? Math.round(parsed) : fallback;
|
||||
}
|
||||
|
||||
function readIntInRange(
|
||||
env: Record<string, string | undefined>,
|
||||
key: string,
|
||||
fallback: number,
|
||||
options: {
|
||||
min: number;
|
||||
max: number;
|
||||
},
|
||||
) {
|
||||
const parsed = Number(env[key]);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const rounded = Math.round(parsed);
|
||||
if (rounded < options.min || rounded > options.max) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return rounded;
|
||||
}
|
||||
|
||||
function readBoolean(
|
||||
env: Record<string, string | undefined>,
|
||||
key: string,
|
||||
fallback: boolean,
|
||||
) {
|
||||
const value = env[key]?.trim().toLowerCase();
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
if (value === '1' || value === 'true' || value === 'yes' || value === 'on') {
|
||||
return true;
|
||||
}
|
||||
if (value === '0' || value === 'false' || value === 'no' || value === 'off') {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
|
||||
const projectRoot = options.projectRoot ?? resolveDefaultProjectRoot();
|
||||
const env = readMergedEnv(projectRoot, options.env ?? process.env);
|
||||
const logsDir = path.join(projectRoot, 'server-node', 'logs');
|
||||
const dataDir = path.join(projectRoot, 'server-node', 'data');
|
||||
const defaultEditorApiEnabled = readString(env, 'NODE_ENV', 'development') !== 'production';
|
||||
const editorApiEnabled = readBoolean(
|
||||
env,
|
||||
'EDITOR_API_ENABLED',
|
||||
defaultEditorApiEnabled,
|
||||
);
|
||||
const smsProvider = readString(
|
||||
env,
|
||||
'SMS_AUTH_PROVIDER',
|
||||
readString(env, 'NODE_ENV', 'development') === 'test' ? 'mock' : 'aliyun',
|
||||
) as AppConfig['smsAuth']['provider'];
|
||||
const smsAccessKeyId = readString(env, 'ALIYUN_SMS_ACCESS_KEY_ID', '');
|
||||
const smsAccessKeySecret = readString(env, 'ALIYUN_SMS_ACCESS_KEY_SECRET', '');
|
||||
const defaultSmsEnabled =
|
||||
smsProvider === 'mock' || Boolean(smsAccessKeyId && smsAccessKeySecret);
|
||||
const wechatProvider = readString(
|
||||
env,
|
||||
'WECHAT_AUTH_PROVIDER',
|
||||
readString(env, 'NODE_ENV', 'development') === 'test' ? 'mock' : 'wechat',
|
||||
) as AppConfig['wechatAuth']['provider'];
|
||||
const wechatAppId = readString(env, 'WECHAT_APP_ID', '');
|
||||
const wechatAppSecret = readString(env, 'WECHAT_APP_SECRET', '');
|
||||
const defaultWechatEnabled =
|
||||
wechatProvider === 'mock' || Boolean(wechatAppId && wechatAppSecret);
|
||||
const refreshSameSite = readString(
|
||||
env,
|
||||
'AUTH_REFRESH_COOKIE_SAME_SITE',
|
||||
'Lax',
|
||||
);
|
||||
|
||||
return {
|
||||
nodeEnv: readString(env, 'NODE_ENV', 'development'),
|
||||
@@ -113,15 +238,26 @@ export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
|
||||
publicDir: path.join(projectRoot, 'public'),
|
||||
logsDir,
|
||||
dataDir,
|
||||
sqlitePath: readString(
|
||||
rawEnv: Object.fromEntries(
|
||||
Object.entries(env).flatMap(([key, value]) =>
|
||||
typeof value === 'string' ? [[key, value]] : [],
|
||||
),
|
||||
),
|
||||
databaseUrl: readString(
|
||||
env,
|
||||
'SQLITE_PATH',
|
||||
path.join(dataDir, 'genarrative.sqlite'),
|
||||
'DATABASE_URL',
|
||||
'postgresql://postgres:postgres@127.0.0.1:5432/genarrative',
|
||||
),
|
||||
serverAddr: readString(env, 'NODE_SERVER_ADDR', ':8081'),
|
||||
logLevel: readString(env, 'LOG_LEVEL', 'info') as AppConfig['logLevel'],
|
||||
editorApiEnabled,
|
||||
assetsApiEnabled: readBoolean(
|
||||
env,
|
||||
'ASSETS_API_ENABLED',
|
||||
editorApiEnabled,
|
||||
),
|
||||
jwtSecret: readString(env, 'JWT_SECRET', 'genarrative-dev-secret'),
|
||||
jwtExpiresIn: readString(env, 'JWT_EXPIRES_IN', '7d'),
|
||||
jwtExpiresIn: readString(env, 'JWT_EXPIRES_IN', '2h'),
|
||||
jwtIssuer: readString(env, 'JWT_ISSUER', 'genarrative-server-node'),
|
||||
llm: {
|
||||
baseUrl: readString(
|
||||
@@ -158,5 +294,177 @@ export function loadConfig(options: LoadConfigOptions = {}): AppConfig {
|
||||
150000,
|
||||
),
|
||||
},
|
||||
smsAuth: {
|
||||
enabled: readBoolean(env, 'SMS_AUTH_ENABLED', defaultSmsEnabled),
|
||||
provider: smsProvider,
|
||||
endpoint: readString(
|
||||
env,
|
||||
'ALIYUN_SMS_ENDPOINT',
|
||||
'dypnsapi.aliyuncs.com',
|
||||
),
|
||||
accessKeyId: smsAccessKeyId,
|
||||
accessKeySecret: smsAccessKeySecret,
|
||||
signName: readString(
|
||||
env,
|
||||
'ALIYUN_SMS_SIGN_NAME',
|
||||
'速通互联验证码',
|
||||
),
|
||||
templateCode: readString(env, 'ALIYUN_SMS_TEMPLATE_CODE', '100001'),
|
||||
templateParamKey: readString(
|
||||
env,
|
||||
'ALIYUN_SMS_TEMPLATE_PARAM_KEY',
|
||||
'code',
|
||||
),
|
||||
countryCode: readString(env, 'ALIYUN_SMS_COUNTRY_CODE', '86'),
|
||||
schemeName: readString(env, 'ALIYUN_SMS_SCHEME_NAME', ''),
|
||||
codeLength: readIntInRange(env, 'ALIYUN_SMS_CODE_LENGTH', 6, {
|
||||
min: 4,
|
||||
max: 8,
|
||||
}),
|
||||
codeType: readIntInRange(env, 'ALIYUN_SMS_CODE_TYPE', 1, {
|
||||
min: 1,
|
||||
max: 7,
|
||||
}),
|
||||
validTimeSeconds: readPositiveInt(
|
||||
env,
|
||||
'ALIYUN_SMS_VALID_TIME_SECONDS',
|
||||
300,
|
||||
),
|
||||
intervalSeconds: readPositiveInt(
|
||||
env,
|
||||
'ALIYUN_SMS_INTERVAL_SECONDS',
|
||||
60,
|
||||
),
|
||||
duplicatePolicy: readIntInRange(
|
||||
env,
|
||||
'ALIYUN_SMS_DUPLICATE_POLICY',
|
||||
1,
|
||||
{ min: 1, max: 2 },
|
||||
),
|
||||
caseAuthPolicy: readIntInRange(
|
||||
env,
|
||||
'ALIYUN_SMS_CASE_AUTH_POLICY',
|
||||
1,
|
||||
{ min: 1, max: 2 },
|
||||
),
|
||||
returnVerifyCode: readBoolean(
|
||||
env,
|
||||
'ALIYUN_SMS_RETURN_VERIFY_CODE',
|
||||
false,
|
||||
),
|
||||
mockVerifyCode: readString(env, 'SMS_AUTH_MOCK_VERIFY_CODE', '123456'),
|
||||
maxSendPerPhonePerDay: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_MAX_SEND_PER_PHONE_PER_DAY',
|
||||
20,
|
||||
),
|
||||
maxSendPerIpPerHour: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_MAX_SEND_PER_IP_PER_HOUR',
|
||||
30,
|
||||
),
|
||||
maxVerifyFailuresPerPhonePerHour: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_MAX_VERIFY_FAILURES_PER_PHONE_PER_HOUR',
|
||||
12,
|
||||
),
|
||||
maxVerifyFailuresPerIpPerHour: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_MAX_VERIFY_FAILURES_PER_IP_PER_HOUR',
|
||||
24,
|
||||
),
|
||||
captchaTtlSeconds: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_CAPTCHA_TTL_SECONDS',
|
||||
180,
|
||||
),
|
||||
captchaTriggerVerifyFailuresPerPhone: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_CAPTCHA_TRIGGER_VERIFY_FAILURES_PER_PHONE',
|
||||
3,
|
||||
),
|
||||
captchaTriggerVerifyFailuresPerIp: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_CAPTCHA_TRIGGER_VERIFY_FAILURES_PER_IP',
|
||||
5,
|
||||
),
|
||||
blockPhoneFailureThreshold: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_BLOCK_PHONE_FAILURE_THRESHOLD',
|
||||
6,
|
||||
),
|
||||
blockIpFailureThreshold: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_BLOCK_IP_FAILURE_THRESHOLD',
|
||||
10,
|
||||
),
|
||||
blockPhoneDurationMinutes: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_BLOCK_PHONE_DURATION_MINUTES',
|
||||
30,
|
||||
),
|
||||
blockIpDurationMinutes: readPositiveInt(
|
||||
env,
|
||||
'SMS_AUTH_BLOCK_IP_DURATION_MINUTES',
|
||||
30,
|
||||
),
|
||||
},
|
||||
wechatAuth: {
|
||||
enabled: readBoolean(env, 'WECHAT_AUTH_ENABLED', defaultWechatEnabled),
|
||||
provider: wechatProvider,
|
||||
appId: wechatAppId,
|
||||
appSecret: wechatAppSecret,
|
||||
authorizeEndpoint: readString(
|
||||
env,
|
||||
'WECHAT_AUTHORIZE_ENDPOINT',
|
||||
'https://open.weixin.qq.com/connect/qrconnect',
|
||||
),
|
||||
accessTokenEndpoint: readString(
|
||||
env,
|
||||
'WECHAT_ACCESS_TOKEN_ENDPOINT',
|
||||
'https://api.weixin.qq.com/sns/oauth2/access_token',
|
||||
),
|
||||
userInfoEndpoint: readString(
|
||||
env,
|
||||
'WECHAT_USER_INFO_ENDPOINT',
|
||||
'https://api.weixin.qq.com/sns/userinfo',
|
||||
),
|
||||
callbackPath: readString(
|
||||
env,
|
||||
'WECHAT_CALLBACK_PATH',
|
||||
'/api/auth/wechat/callback',
|
||||
),
|
||||
defaultRedirectPath: readString(env, 'WECHAT_REDIRECT_PATH', '/'),
|
||||
mockUserId: readString(env, 'WECHAT_MOCK_USER_ID', 'mock_wechat_user'),
|
||||
mockUnionId: readString(env, 'WECHAT_MOCK_UNION_ID', 'mock_wechat_union'),
|
||||
mockDisplayName: readString(env, 'WECHAT_MOCK_DISPLAY_NAME', '微信旅人'),
|
||||
mockAvatarUrl: readString(env, 'WECHAT_MOCK_AVATAR_URL', ''),
|
||||
},
|
||||
authSession: {
|
||||
refreshCookieName: readString(
|
||||
env,
|
||||
'AUTH_REFRESH_COOKIE_NAME',
|
||||
'genarrative_refresh_session',
|
||||
),
|
||||
refreshSessionTtlDays: readPositiveInt(
|
||||
env,
|
||||
'AUTH_REFRESH_SESSION_TTL_DAYS',
|
||||
30,
|
||||
),
|
||||
refreshCookieSecure: readBoolean(
|
||||
env,
|
||||
'AUTH_REFRESH_COOKIE_SECURE',
|
||||
readString(env, 'NODE_ENV', 'development') === 'production',
|
||||
),
|
||||
refreshCookieSameSite:
|
||||
refreshSameSite === 'None' || refreshSameSite === 'Strict'
|
||||
? (refreshSameSite as AppConfig['authSession']['refreshCookieSameSite'])
|
||||
: 'Lax',
|
||||
refreshCookiePath: readString(
|
||||
env,
|
||||
'AUTH_REFRESH_COOKIE_PATH',
|
||||
'/api/auth',
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user