Allow local env files to reliably override authentication feature flags (SMS/WeChat) by whitelisting keys in scripts/dev-utils.mjs and adding a unit test. Add SMS checks to scripts/check-api-server-env.mjs. Make server config.parse_bool tolerant of shell-wrapped quoted values (e.g. '"true"') and add tests so SMS_AUTH_ENABLED is parsed correctly when shells supply quotes. Update docs to clarify SMS env behaviour, restart requirements, and add guidance + a CSS fallback for old mobile browsers (QQ/X5) so public cover images render even when aspect-ratio is unsupported. Also include related frontend test and component adjustments and add puzzle onboarding handlers/endpoints in server-rs/crates/api-server/src/puzzle.rs.
116 lines
2.8 KiB
JavaScript
116 lines
2.8 KiB
JavaScript
import {existsSync, mkdirSync, readFileSync} from 'node:fs';
|
|
import {dirname, isAbsolute, resolve} from 'node:path';
|
|
|
|
export const LOCAL_ENV_FILES = ['.env', '.env.local', '.env.secrets.local'];
|
|
const LOCAL_ENV_OVERRIDE_KEYS = new Set([
|
|
'SMS_AUTH_ENABLED',
|
|
'SMS_AUTH_PROVIDER',
|
|
'SMS_AUTH_MOCK_VERIFY_CODE',
|
|
'WECHAT_AUTH_ENABLED',
|
|
'WECHAT_AUTH_PROVIDER',
|
|
]);
|
|
|
|
export function buildProtectedEnvKeys(baseEnv) {
|
|
return new Set(
|
|
Object.entries(baseEnv)
|
|
.filter(([, value]) => String(value ?? '').trim())
|
|
.map(([key]) => key),
|
|
);
|
|
}
|
|
|
|
export function loadEnvFile(path, target, protectedKeys) {
|
|
if (!existsSync(path)) {
|
|
return;
|
|
}
|
|
|
|
const rawText = readFileSync(path, 'utf8');
|
|
for (const rawLine of rawText.split(/\r?\n/u)) {
|
|
const line = rawLine.trim();
|
|
if (!line || line.startsWith('#')) {
|
|
continue;
|
|
}
|
|
|
|
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u);
|
|
if (!match) {
|
|
continue;
|
|
}
|
|
|
|
const [, key, rawValue] = match;
|
|
if (protectedKeys.has(key) && !LOCAL_ENV_OVERRIDE_KEYS.has(key)) {
|
|
continue;
|
|
}
|
|
|
|
target[key] = rawValue.replace(/^['"]|['"]$/gu, '');
|
|
}
|
|
}
|
|
|
|
export function loadApiServerEnv(repoRootPath, target, protectedKeys) {
|
|
const resolvedProtectedKeys =
|
|
protectedKeys ?? buildProtectedEnvKeys(process.env);
|
|
|
|
// shell > .env > .env.local > .env.secrets.local
|
|
for (const fileName of LOCAL_ENV_FILES) {
|
|
loadEnvFile(resolve(repoRootPath, fileName), target, resolvedProtectedKeys);
|
|
}
|
|
}
|
|
|
|
export function mergeApiServerEnv(repoRootPath, baseEnv = process.env) {
|
|
const mergedEnv = {...baseEnv};
|
|
loadApiServerEnv(repoRootPath, mergedEnv, buildProtectedEnvKeys(baseEnv));
|
|
return mergedEnv;
|
|
}
|
|
|
|
export function formatApiServerLogTimestamp(date = new Date()) {
|
|
const pad = (value) => String(value).padStart(2, '0');
|
|
|
|
return [
|
|
date.getFullYear(),
|
|
pad(date.getMonth() + 1),
|
|
pad(date.getDate()),
|
|
'-',
|
|
pad(date.getHours()),
|
|
pad(date.getMinutes()),
|
|
pad(date.getSeconds()),
|
|
].join('');
|
|
}
|
|
|
|
export function resolveApiServerLogFile(
|
|
repoRootPath,
|
|
env = process.env,
|
|
now = new Date(),
|
|
) {
|
|
const explicitLogFile = String(
|
|
env.GENARRATIVE_API_SERVER_LOG_FILE ?? '',
|
|
).trim();
|
|
|
|
if (explicitLogFile) {
|
|
return isAbsolute(explicitLogFile)
|
|
? explicitLogFile
|
|
: resolve(repoRootPath, explicitLogFile);
|
|
}
|
|
|
|
const logDir =
|
|
String(env.GENARRATIVE_API_SERVER_LOG_DIR ?? '').trim() ||
|
|
'logs/api-server';
|
|
const resolvedLogDir = isAbsolute(logDir)
|
|
? logDir
|
|
: resolve(repoRootPath, logDir);
|
|
|
|
return resolve(
|
|
resolvedLogDir,
|
|
`api-server-${formatApiServerLogTimestamp(now)}.log`,
|
|
);
|
|
}
|
|
|
|
export function ensureParentDir(filePath) {
|
|
mkdirSync(dirname(filePath), {recursive: true});
|
|
}
|
|
|
|
export function resolveClientHost(hostName) {
|
|
if (hostName === '0.0.0.0' || hostName === '::') {
|
|
return '127.0.0.1';
|
|
}
|
|
|
|
return hostName;
|
|
}
|