import express from "express"; import path from "path"; import { createServer as createViteServer } from "vite"; import pg from "pg"; import dotenv from "dotenv"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import nodemailer from "nodemailer"; dotenv.config(); const app = express(); const PORT = parseInt(process.env.PORT || "3000", 10); const JWT_SECRET = process.env.JWT_SECRET || "pinnacle_jwt_secret_key_2026_prod"; app.use(express.json({ limit: "50mb" })); // Helper Functions for Password & Token Security function hashPassword(password: string): string { return bcrypt.hashSync(password, 10); } function verifyPassword(password: string, hashOrPlain?: string): boolean { if (!hashOrPlain) return false; if (hashOrPlain.startsWith("$2a$") || hashOrPlain.startsWith("$2b$")) { return bcrypt.compareSync(password, hashOrPlain); } // Fallback for legacy case-insensitive or exact plain match, then upgrade return password.toLowerCase() === hashOrPlain.toLowerCase(); } function generateToken(user: any): string { return jwt.sign( { id: user.id, username: user.username, role: user.role, fullName: user.fullName, email: user.email, dept: user.dept }, JWT_SECRET, { expiresIn: "24h" } ); } // PostgreSQL Connection Pool Setup let dbPool: pg.Pool | null = null; let isPostgresConnected = false; const dbUrl = process.env.DATABASE_URL || (process.env.POSTGRES_HOST ? `postgres://${process.env.POSTGRES_USER || 'postgres'}:${process.env.POSTGRES_PASSWORD || 'postgres_password'}@${process.env.POSTGRES_HOST}:${process.env.POSTGRES_PORT || 5432}/${process.env.POSTGRES_DB || 'pinnacle_db'}` : null); if (dbUrl) { try { dbPool = new pg.Pool({ connectionString: dbUrl, ssl: process.env.PGSSLMODE === "require" ? { rejectUnauthorized: false } : false, connectionTimeoutMillis: 3000, }); // Test DB connection and auto-create tables dbPool.query(` CREATE TABLE IF NOT EXISTS pinnacle_store ( id VARCHAR(50) PRIMARY KEY DEFAULT 'default', data JSONB NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS pinnacle_users ( id SERIAL PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT 'viewer', full_name VARCHAR(100) NOT NULL, email VARCHAR(100) NOT NULL, dept VARCHAR(100) NOT NULL, active BOOLEAN DEFAULT TRUE, last_login TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); `).then(() => { isPostgresConnected = true; console.log("Connected to PostgreSQL database successfully."); }).catch(err => { console.warn("PostgreSQL connection notice:", err.message, "(Falling back to memory storage for dev environment)"); }); } catch (err: any) { console.warn("PostgreSQL Pool init notice:", err.message); } } // In-Memory Fallback State (when PostgreSQL container is not connected) let memoryStore: any = null; // Auth Middleware to verify JWT tokens const authenticateToken = (req: any, res: any, next: any) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: "Access denied. Authentication token missing." }); } try { const verified = jwt.verify(token, JWT_SECRET); req.user = verified; next(); } catch (err) { return res.status(403).json({ error: "Invalid or expired authentication token." }); } }; // API ROUTES // Health Check app.get("/api/health", async (req, res) => { let pgStatus = "disconnected"; if (dbPool) { try { await dbPool.query("SELECT 1"); pgStatus = "connected"; isPostgresConnected = true; } catch { pgStatus = "error"; isPostgresConnected = false; } } res.json({ status: "ok", app: "Pinnacle Decision Portal", postgres: pgStatus, timestamp: new Date().toISOString(), environment: process.env.NODE_ENV || "development", }); }); // User Authentication: Login app.post("/api/auth/login", async (req, res) => { const { username, password } = req.body; if (!username || !password) { return res.status(400).json({ error: "Username and password are required." }); } let users = memoryStore?.users || []; // Search user in memory store or default let user = users.find((u: any) => u.username.toLowerCase() === username.toLowerCase()); if (!user && !memoryStore) { // Check initial defaults const defaults = [ { id: 1, username: 'admin', password: 'Admin', role: 'admin', fullName: 'System Administrator', email: 'admin@pinnacle.local', dept: 'Executive Management', active: true }, { id: 2, username: 'editor', password: 'Editor', role: 'editor', fullName: 'Project Manager', email: 'pm@pinnacle.local', dept: 'Project Management Office', active: true }, { id: 3, username: 'viewer', password: 'Viewer', role: 'viewer', fullName: 'Board Observer', email: 'observer@pinnacle.local', dept: 'Board of Directors', active: true } ]; user = defaults.find(u => u.username.toLowerCase() === username.toLowerCase()); } if (!user) { return res.status(401).json({ error: "Invalid username or password." }); } if (user.active === false) { return res.status(403).json({ error: "Your account has been deactivated by an administrator." }); } const isValid = verifyPassword(password, user.password); if (!isValid) { return res.status(401).json({ error: "Invalid username or password." }); } // Upgrade password to hash if stored plain if (!user.password.startsWith("$2a$") && !user.password.startsWith("$2b$")) { user.password = hashPassword(password); } const now = new Date().toISOString(); user.lastLogin = now; const token = generateToken(user); // Return user without sending sensitive hash back directly const { password: _, ...userWithoutPassword } = user; res.json({ success: true, token, user: userWithoutPassword }); }); // User Authentication: Session Verification app.get("/api/auth/me", authenticateToken, (req: any, res: any) => { res.json({ authenticated: true, user: req.user }); }); // User Authentication: Change Password app.post("/api/auth/change-password", authenticateToken, (req: any, res: any) => { const { currentPassword, newPassword } = req.body; const username = req.user.username; if (!currentPassword || !newPassword) { return res.status(400).json({ error: "Current password and new password are required." }); } if (newPassword.length < 6) { return res.status(400).json({ error: "New password must be at least 6 characters long." }); } let users = memoryStore?.users || []; let user = users.find((u: any) => u.username.toLowerCase() === username.toLowerCase()); if (!user) { return res.status(404).json({ error: "User account not found." }); } const isValid = verifyPassword(currentPassword, user.password); if (!isValid) { return res.status(401).json({ error: "Current password is incorrect." }); } user.password = hashPassword(newPassword); res.json({ success: true, message: "Password updated successfully." }); }); // Backend Secure SMTP Config Store let backendSmtpConfig = { host: process.env.SMTP_HOST || 'smtp.mailtrap.io', port: parseInt(process.env.SMTP_PORT || '587', 10), user: process.env.SMTP_USER || '', pass: process.env.SMTP_PASS || '', secure: process.env.SMTP_SECURE === "true", fromName: process.env.SMTP_FROM_NAME || 'Pinnacle Decision Portal', fromEmail: process.env.SMTP_FROM_EMAIL || 'notifications@pinnacle.local', enabled: true, notifyOnNewDecision: true, notifyOnHighRisk: true, notifyOnUserCreated: true }; function createSmtpTransporter(cfg?: any) { const host = cfg?.host || backendSmtpConfig.host || process.env.SMTP_HOST; const port = cfg?.port || backendSmtpConfig.port || parseInt(process.env.SMTP_PORT || "587", 10); const user = cfg?.user || backendSmtpConfig.user || process.env.SMTP_USER; const pass = (cfg?.pass && cfg.pass !== "••••••••••••") ? cfg.pass : (backendSmtpConfig.pass || process.env.SMTP_PASS); const secure = cfg?.secure ?? backendSmtpConfig.secure; if (!host || !user || !pass) { return null; } return nodemailer.createTransport({ host, port, secure, auth: { user, pass }, tls: { rejectUnauthorized: false } }); } // Backend SMTP Management API app.get("/api/smtp/config", (req: any, res: any) => { res.json({ host: backendSmtpConfig.host, port: backendSmtpConfig.port, user: backendSmtpConfig.user, secure: backendSmtpConfig.secure, fromName: backendSmtpConfig.fromName, fromEmail: backendSmtpConfig.fromEmail, enabled: backendSmtpConfig.enabled, notifyOnNewDecision: backendSmtpConfig.notifyOnNewDecision, notifyOnHighRisk: backendSmtpConfig.notifyOnHighRisk, notifyOnUserCreated: backendSmtpConfig.notifyOnUserCreated, hasPassword: Boolean(backendSmtpConfig.pass && backendSmtpConfig.pass.length > 0), pass: backendSmtpConfig.pass ? "••••••••••••" : "" }); }); app.post("/api/smtp/config", (req: any, res: any) => { const { host, port, user, pass, secure, fromName, fromEmail, enabled, notifyOnNewDecision, notifyOnHighRisk, notifyOnUserCreated } = req.body; let updatedPass = backendSmtpConfig.pass; if (pass && pass !== "••••••••••••") { updatedPass = pass; } backendSmtpConfig = { host: host ?? backendSmtpConfig.host, port: parseInt(port, 10) || backendSmtpConfig.port, user: user ?? backendSmtpConfig.user, pass: updatedPass, secure: Boolean(secure), fromName: fromName ?? backendSmtpConfig.fromName, fromEmail: fromEmail ?? backendSmtpConfig.fromEmail, enabled: enabled !== undefined ? Boolean(enabled) : backendSmtpConfig.enabled, notifyOnNewDecision: notifyOnNewDecision !== undefined ? Boolean(notifyOnNewDecision) : backendSmtpConfig.notifyOnNewDecision, notifyOnHighRisk: notifyOnHighRisk !== undefined ? Boolean(notifyOnHighRisk) : backendSmtpConfig.notifyOnHighRisk, notifyOnUserCreated: notifyOnUserCreated !== undefined ? Boolean(notifyOnUserCreated) : backendSmtpConfig.notifyOnUserCreated }; return res.json({ success: true, message: "Backend SMTP configuration updated securely.", config: { ...backendSmtpConfig, pass: backendSmtpConfig.pass ? "••••••••••••" : "" } }); }); // SMTP Test Endpoint app.post("/api/notifications/smtp-test", async (req: any, res: any) => { const { recipientEmail, host, port, user, pass, secure, fromName, fromEmail } = req.body; const targetRecipient = recipientEmail || "admin@pinnacle.local"; const activeCfg = { host: host || backendSmtpConfig.host, port: port || backendSmtpConfig.port, user: user || backendSmtpConfig.user, pass: (pass && pass !== "••••••••••••") ? pass : backendSmtpConfig.pass, secure: secure !== undefined ? secure : backendSmtpConfig.secure, fromName: fromName || backendSmtpConfig.fromName, fromEmail: fromEmail || backendSmtpConfig.fromEmail }; const senderName = activeCfg.fromName; const senderEmail = activeCfg.fromEmail; const transporter = createSmtpTransporter(activeCfg); const mailOptions = { from: `"${senderName}" <${senderEmail}>`, to: targetRecipient, subject: "Pinnacle Portal - SMTP Test Notification", html: `
This is a test notification confirming that your Backend SMTP Server Configuration is active and connected.
Automated System Dispatch - Pinnacle Governance Engine