* fix: restore password migration COALESCE and add trust proxy diagnostics This commit addresses two critical issues affecting user login: 1. Password Migration Fix: - Restore COALESCE(password_digest, password) in migration 20260420000004 - The COALESCE fix from commit d1aa6086 was accidentally reverted - Handles both v1.0.0 column naming (password) and current (password_digest) - Allows users from v1.0.0 to successfully login after migration 2. Trust Proxy Configuration Improvements: - Add startup logging to show trust proxy configuration value - Add config parsing logging to diagnose env variable issues - Add trust proxy status to /health endpoint - Improve error messages for ERR_ERL_UNEXPECTED_X_FORWARDED_FOR - Update .env.example with comprehensive trust proxy documentation 3. Diagnostic Tools: - Add backend/scripts/diagnose-password-migration.js script - Script checks database schema and identifies affected users - Provides actionable recovery steps 4. Documentation: - Add docs/troubleshooting/migration-issues.md - Covers password migration issues and trust proxy configuration - Includes Docker-specific troubleshooting steps - Provides step-by-step recovery procedures Files changed: - backend/migrations/20260420000004-make-password-optional.js (restore COALESCE) - backend/app.js (add trust proxy logging) - backend/config/config.js (add config parsing logging) - backend/shared/middleware/errorHandler.js (better trust proxy errors) - backend/scripts/diagnose-password-migration.js (new diagnostic tool) - backend/.env.example (improved trust proxy documentation) - docs/troubleshooting/migration-issues.md (new troubleshooting guide) * docs: remove troubleshooting documentation file * fix: resolve CodeQL false positives in diagnostic script Rename variables to avoid CodeQL flagging them as sensitive data: - hasPassword -> passwordColumnExists - hasPasswordDigest -> passwordDigestColumnExists - users_with_password -> count_with_digest - users_without_password -> count_without_digest These variables only contain booleans and counts, not actual password data.
201 lines
5.9 KiB
JavaScript
201 lines
5.9 KiB
JavaScript
const path = require('path');
|
|
|
|
if (
|
|
process.env.NODE_ENV !== 'production' &&
|
|
process.env.NODE_ENV !== 'development' &&
|
|
process.env.NODE_ENV !== 'test'
|
|
) {
|
|
console.error(
|
|
"NODE_ENV should be one of 'production', 'development' or 'test'."
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
const environment = process.env.NODE_ENV;
|
|
const production = process.env.NODE_ENV === 'production';
|
|
const projectRootPath = path.join(__dirname, '..'); // backend root path
|
|
|
|
const credentials = {
|
|
google: {
|
|
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
redirectUri:
|
|
process.env.GOOGLE_REDIRECT_URI ||
|
|
'http://localhost:3002/api/calendar/oauth/callback',
|
|
},
|
|
};
|
|
|
|
const defaultHost = environment === 'test' ? '127.0.0.1' : '0.0.0.0';
|
|
|
|
const emailConfig = {
|
|
enabled: process.env.ENABLE_EMAIL === 'true',
|
|
smtp: {
|
|
host: process.env.EMAIL_SMTP_HOST,
|
|
port: process.env.EMAIL_SMTP_PORT
|
|
? parseInt(process.env.EMAIL_SMTP_PORT, 10)
|
|
: 587,
|
|
secure: process.env.EMAIL_SMTP_SECURE === 'true',
|
|
auth: {
|
|
user: process.env.EMAIL_SMTP_USERNAME,
|
|
pass: process.env.EMAIL_SMTP_PASSWORD,
|
|
},
|
|
},
|
|
from: {
|
|
address: process.env.EMAIL_FROM_ADDRESS,
|
|
name: process.env.EMAIL_FROM_NAME || 'Tududi',
|
|
},
|
|
};
|
|
|
|
const registrationConfig = {
|
|
tokenExpiryHours: process.env.REGISTRATION_TOKEN_EXPIRY_HOURS
|
|
? parseInt(process.env.REGISTRATION_TOKEN_EXPIRY_HOURS, 10)
|
|
: 24,
|
|
};
|
|
|
|
const config = {
|
|
allowedOrigins: process.env.TUDUDI_ALLOWED_ORIGINS
|
|
? process.env.TUDUDI_ALLOWED_ORIGINS.split(',').map((origin) =>
|
|
origin.trim()
|
|
)
|
|
: [
|
|
'http://localhost:8080',
|
|
'http://localhost:9292',
|
|
'http://127.0.0.1:8080',
|
|
'http://127.0.0.1:9292',
|
|
],
|
|
|
|
dbFile:
|
|
process.env.DB_FILE ||
|
|
path.join(projectRootPath, 'db', `${environment}.sqlite3`),
|
|
|
|
disableScheduler: process.env.DISABLE_SCHEDULER === 'true',
|
|
|
|
disableTelegram: process.env.DISABLE_TELEGRAM === 'true',
|
|
|
|
email: process.env.TUDUDI_USER_EMAIL,
|
|
|
|
environment,
|
|
|
|
frontendUrl: process.env.FRONTEND_URL || 'http://localhost:8080',
|
|
|
|
backendUrl: process.env.BACKEND_URL || 'http://localhost:3002',
|
|
|
|
// Some CI/sandbox environments disallow binding to 0.0.0.0, so force
|
|
// loopback for tests unless HOST is explicitly provided.
|
|
host: process.env.HOST || defaultHost,
|
|
|
|
port: process.env.PORT || 3002,
|
|
|
|
password: process.env.TUDUDI_USER_PASSWORD,
|
|
|
|
production,
|
|
|
|
secret:
|
|
process.env.TUDUDI_SESSION_SECRET ||
|
|
require('crypto').randomBytes(64).toString('hex'),
|
|
|
|
credentials,
|
|
|
|
emailConfig,
|
|
|
|
registrationConfig,
|
|
|
|
uploadPath:
|
|
process.env.TUDUDI_UPLOAD_PATH || path.join(projectRootPath, 'uploads'),
|
|
|
|
// API Documentation (Swagger)
|
|
swagger: {
|
|
enabled: process.env.SWAGGER_ENABLED !== 'false',
|
|
},
|
|
|
|
trustProxy: (() => {
|
|
const val = process.env.TUDUDI_TRUST_PROXY;
|
|
if (val === undefined || val === '') {
|
|
console.log('[Config] TUDUDI_TRUST_PROXY not set, using false');
|
|
return false;
|
|
}
|
|
if (val === 'true') {
|
|
console.log(
|
|
'[Config] TUDUDI_TRUST_PROXY=true parsed as boolean true'
|
|
);
|
|
return true;
|
|
}
|
|
if (val === 'false') {
|
|
console.log(
|
|
'[Config] TUDUDI_TRUST_PROXY=false parsed as boolean false'
|
|
);
|
|
return false;
|
|
}
|
|
const num = Number(val);
|
|
if (!isNaN(num) && val.trim() !== '') {
|
|
console.log(
|
|
`[Config] TUDUDI_TRUST_PROXY=${val} parsed as number ${num}`
|
|
);
|
|
return num;
|
|
}
|
|
console.log(`[Config] TUDUDI_TRUST_PROXY=${val} parsed as string`);
|
|
return val;
|
|
})(),
|
|
|
|
// Rate limiting configuration
|
|
rateLimiting: {
|
|
// Disable rate limiting in test environment
|
|
enabled:
|
|
process.env.RATE_LIMITING_ENABLED !== 'false' &&
|
|
environment !== 'test',
|
|
|
|
// Authentication endpoints (login, register)
|
|
auth: {
|
|
windowMs:
|
|
parseInt(process.env.RATE_LIMIT_AUTH_WINDOW_MS) ||
|
|
15 * 60 * 1000, // 15 minutes
|
|
max: parseInt(process.env.RATE_LIMIT_AUTH_MAX) || 5, // 5 requests per window
|
|
},
|
|
|
|
// General API for unauthenticated requests
|
|
api: {
|
|
windowMs:
|
|
parseInt(process.env.RATE_LIMIT_API_WINDOW_MS) ||
|
|
15 * 60 * 1000, // 15 minutes
|
|
max: parseInt(process.env.RATE_LIMIT_API_MAX) || 100, // 100 requests per window
|
|
},
|
|
|
|
// Authenticated API requests
|
|
authenticatedApi: {
|
|
windowMs:
|
|
parseInt(process.env.RATE_LIMIT_AUTH_API_WINDOW_MS) ||
|
|
15 * 60 * 1000, // 15 minutes
|
|
max: parseInt(process.env.RATE_LIMIT_AUTH_API_MAX) || 1000, // 1000 requests per window
|
|
},
|
|
|
|
// Resource creation endpoints
|
|
createResource: {
|
|
windowMs:
|
|
parseInt(process.env.RATE_LIMIT_CREATE_WINDOW_MS) ||
|
|
15 * 60 * 1000, // 15 minutes
|
|
max: parseInt(process.env.RATE_LIMIT_CREATE_MAX) || 50, // 50 requests per window
|
|
},
|
|
|
|
// API key management endpoints
|
|
apiKeyManagement: {
|
|
windowMs:
|
|
parseInt(process.env.RATE_LIMIT_API_KEY_WINDOW_MS) ||
|
|
60 * 60 * 1000, // 1 hour
|
|
max: parseInt(process.env.RATE_LIMIT_API_KEY_MAX) || 10, // 10 requests per window
|
|
},
|
|
},
|
|
};
|
|
|
|
console.log(`Using database file '${config.dbFile}'`);
|
|
|
|
function setConfig({ dbFile } = {}) {
|
|
if (dbFile != null) {
|
|
config.dbFile = dbFile;
|
|
}
|
|
}
|
|
|
|
function getConfig() {
|
|
return config;
|
|
}
|
|
|
|
module.exports = { setConfig, getConfig };
|