mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:12:09 +01:00
feat: comprehensive security and architecture improvements
- Add Zod validation schemas with strong password requirements (12+ chars, complexity) - Implement rate limiting for authentication endpoints (registration, password reset) - Remove duplicate MetricCard component, consolidate to ui/metric-card.tsx - Update README.md to use pnpm commands consistently - Enhance authentication security with 12-round bcrypt hashing - Add comprehensive input validation for all API endpoints - Fix security vulnerabilities in user registration and password reset flows 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
74
lib/env.ts
74
lib/env.ts
@ -7,20 +7,22 @@ import { dirname, join } from "path";
|
||||
* Parse environment variable value by removing quotes, comments, and trimming whitespace
|
||||
*/
|
||||
function parseEnvValue(value: string | undefined): string {
|
||||
if (!value) return '';
|
||||
if (!value) return "";
|
||||
|
||||
// Trim whitespace
|
||||
let cleaned = value.trim();
|
||||
|
||||
// Remove inline comments (everything after #)
|
||||
const commentIndex = cleaned.indexOf('#');
|
||||
const commentIndex = cleaned.indexOf("#");
|
||||
if (commentIndex !== -1) {
|
||||
cleaned = cleaned.substring(0, commentIndex).trim();
|
||||
}
|
||||
|
||||
// Remove surrounding quotes (both single and double)
|
||||
if ((cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||
(cleaned.startsWith("'") && cleaned.endsWith("'"))) {
|
||||
if (
|
||||
(cleaned.startsWith('"') && cleaned.endsWith('"')) ||
|
||||
(cleaned.startsWith("'") && cleaned.endsWith("'"))
|
||||
) {
|
||||
cleaned = cleaned.slice(1, -1);
|
||||
}
|
||||
|
||||
@ -30,7 +32,10 @@ function parseEnvValue(value: string | undefined): string {
|
||||
/**
|
||||
* Parse integer with fallback to default value
|
||||
*/
|
||||
function parseIntWithDefault(value: string | undefined, defaultValue: number): number {
|
||||
function parseIntWithDefault(
|
||||
value: string | undefined,
|
||||
defaultValue: number
|
||||
): number {
|
||||
const cleaned = parseEnvValue(value);
|
||||
if (!cleaned) return defaultValue;
|
||||
|
||||
@ -41,17 +46,19 @@ function parseIntWithDefault(value: string | undefined, defaultValue: number): n
|
||||
// Load environment variables from .env.local
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const envPath = join(__dirname, '..', '.env.local');
|
||||
const envPath = join(__dirname, "..", ".env.local");
|
||||
|
||||
// Load .env.local if it exists
|
||||
try {
|
||||
const envFile = readFileSync(envPath, 'utf8');
|
||||
const envVars = envFile.split('\n').filter(line => line.trim() && !line.startsWith('#'));
|
||||
const envFile = readFileSync(envPath, "utf8");
|
||||
const envVars = envFile
|
||||
.split("\n")
|
||||
.filter((line) => line.trim() && !line.startsWith("#"));
|
||||
|
||||
envVars.forEach(line => {
|
||||
const [key, ...valueParts] = line.split('=');
|
||||
envVars.forEach((line) => {
|
||||
const [key, ...valueParts] = line.split("=");
|
||||
if (key && valueParts.length > 0) {
|
||||
const rawValue = valueParts.join('=');
|
||||
const rawValue = valueParts.join("=");
|
||||
const cleanedValue = parseEnvValue(rawValue);
|
||||
if (!process.env[key.trim()]) {
|
||||
process.env[key.trim()] = cleanedValue;
|
||||
@ -67,21 +74,34 @@ try {
|
||||
*/
|
||||
export const env = {
|
||||
// NextAuth
|
||||
NEXTAUTH_URL: parseEnvValue(process.env.NEXTAUTH_URL) || 'http://localhost:3000',
|
||||
NEXTAUTH_SECRET: parseEnvValue(process.env.NEXTAUTH_SECRET) || '',
|
||||
NODE_ENV: parseEnvValue(process.env.NODE_ENV) || 'development',
|
||||
NEXTAUTH_URL:
|
||||
parseEnvValue(process.env.NEXTAUTH_URL) || "http://localhost:3000",
|
||||
NEXTAUTH_SECRET: parseEnvValue(process.env.NEXTAUTH_SECRET) || "",
|
||||
NODE_ENV: parseEnvValue(process.env.NODE_ENV) || "development",
|
||||
|
||||
// OpenAI
|
||||
OPENAI_API_KEY: parseEnvValue(process.env.OPENAI_API_KEY) || '',
|
||||
OPENAI_API_KEY: parseEnvValue(process.env.OPENAI_API_KEY) || "",
|
||||
|
||||
// Scheduler Configuration
|
||||
SCHEDULER_ENABLED: parseEnvValue(process.env.SCHEDULER_ENABLED) === 'true',
|
||||
CSV_IMPORT_INTERVAL: parseEnvValue(process.env.CSV_IMPORT_INTERVAL) || '*/15 * * * *',
|
||||
IMPORT_PROCESSING_INTERVAL: parseEnvValue(process.env.IMPORT_PROCESSING_INTERVAL) || '*/5 * * * *',
|
||||
IMPORT_PROCESSING_BATCH_SIZE: parseIntWithDefault(process.env.IMPORT_PROCESSING_BATCH_SIZE, 50),
|
||||
SESSION_PROCESSING_INTERVAL: parseEnvValue(process.env.SESSION_PROCESSING_INTERVAL) || '0 * * * *',
|
||||
SESSION_PROCESSING_BATCH_SIZE: parseIntWithDefault(process.env.SESSION_PROCESSING_BATCH_SIZE, 0),
|
||||
SESSION_PROCESSING_CONCURRENCY: parseIntWithDefault(process.env.SESSION_PROCESSING_CONCURRENCY, 5),
|
||||
SCHEDULER_ENABLED: parseEnvValue(process.env.SCHEDULER_ENABLED) === "true",
|
||||
CSV_IMPORT_INTERVAL:
|
||||
parseEnvValue(process.env.CSV_IMPORT_INTERVAL) || "*/15 * * * *",
|
||||
IMPORT_PROCESSING_INTERVAL:
|
||||
parseEnvValue(process.env.IMPORT_PROCESSING_INTERVAL) || "*/5 * * * *",
|
||||
IMPORT_PROCESSING_BATCH_SIZE: parseIntWithDefault(
|
||||
process.env.IMPORT_PROCESSING_BATCH_SIZE,
|
||||
50
|
||||
),
|
||||
SESSION_PROCESSING_INTERVAL:
|
||||
parseEnvValue(process.env.SESSION_PROCESSING_INTERVAL) || "0 * * * *",
|
||||
SESSION_PROCESSING_BATCH_SIZE: parseIntWithDefault(
|
||||
process.env.SESSION_PROCESSING_BATCH_SIZE,
|
||||
0
|
||||
),
|
||||
SESSION_PROCESSING_CONCURRENCY: parseIntWithDefault(
|
||||
process.env.SESSION_PROCESSING_CONCURRENCY,
|
||||
5
|
||||
),
|
||||
|
||||
// Server
|
||||
PORT: parseIntWithDefault(process.env.PORT, 3000),
|
||||
@ -94,11 +114,11 @@ export function validateEnv(): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!env.NEXTAUTH_SECRET) {
|
||||
errors.push('NEXTAUTH_SECRET is required');
|
||||
errors.push("NEXTAUTH_SECRET is required");
|
||||
}
|
||||
|
||||
if (!env.OPENAI_API_KEY && env.NODE_ENV === 'production') {
|
||||
errors.push('OPENAI_API_KEY is required in production');
|
||||
if (!env.OPENAI_API_KEY && env.NODE_ENV === "production") {
|
||||
errors.push("OPENAI_API_KEY is required in production");
|
||||
}
|
||||
|
||||
return {
|
||||
@ -132,14 +152,14 @@ export function getSchedulerConfig() {
|
||||
* Log environment configuration (safe for production)
|
||||
*/
|
||||
export function logEnvConfig(): void {
|
||||
console.log('[Environment] Configuration:');
|
||||
console.log("[Environment] Configuration:");
|
||||
console.log(` NODE_ENV: ${env.NODE_ENV}`);
|
||||
console.log(` NEXTAUTH_URL: ${env.NEXTAUTH_URL}`);
|
||||
console.log(` SCHEDULER_ENABLED: ${env.SCHEDULER_ENABLED}`);
|
||||
console.log(` PORT: ${env.PORT}`);
|
||||
|
||||
if (env.SCHEDULER_ENABLED) {
|
||||
console.log(' Scheduler intervals:');
|
||||
console.log(" Scheduler intervals:");
|
||||
console.log(` CSV Import: ${env.CSV_IMPORT_INTERVAL}`);
|
||||
console.log(` Import Processing: ${env.IMPORT_PROCESSING_INTERVAL}`);
|
||||
console.log(` Session Processing: ${env.SESSION_PROCESSING_INTERVAL}`);
|
||||
|
||||
Reference in New Issue
Block a user