133 lines
4.9 KiB
JavaScript
133 lines
4.9 KiB
JavaScript
// chat-server — Phase 9 §9.1 of CONSOLIDATION_PLAN.md.
|
|
//
|
|
// ESM conversion + in-process authenticate() defense-in-depth. Previously this
|
|
// service relied on the Caddy `forward_auth` gate alone — `localhost:3014`
|
|
// was reachable unauthenticated. Now:
|
|
// 1. Bound to 127.0.0.1 (was 0.0.0.0) so direct-port access is impossible.
|
|
// 2. authenticate() runs against an in-process `inventory_db` pool before
|
|
// any route handler sees the request.
|
|
//
|
|
// Two pools intentionally:
|
|
// - `inventoryPool`: used by authenticate() for users/permissions lookups
|
|
// against the main inventory_db (matches DB_* env vars).
|
|
// - `pool` (set as global.pool for routes.js): the existing
|
|
// `rocketchat_converted` pool driven by CHAT_DB_* env vars. routes.js
|
|
// reads global.pool throughout — no handler-body changes needed.
|
|
|
|
import { config as loadEnv } from 'dotenv';
|
|
import express from 'express';
|
|
import cors from 'cors';
|
|
import morgan from 'morgan';
|
|
import pg from 'pg';
|
|
import path from 'node:path';
|
|
import fs from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { authenticate } from '../shared/auth/middleware.js';
|
|
import { corsOptions } from '../shared/cors/policy.js';
|
|
import { errorHandler } from '../shared/errors/handler.js';
|
|
import { requestLog } from '../shared/logging/request-log.js';
|
|
|
|
import chatRoutes from './routes.js';
|
|
|
|
const { Pool } = pg;
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
|
// Env layering matches dashboard-server (Deviation #18): shared .env wins on
|
|
// collisions for security-critical vars, local .env supplies CHAT_DB_*.
|
|
const sharedEnvPath = '/var/www/inventory/.env';
|
|
const localEnvPath = path.resolve(__dirname, '.env');
|
|
if (fs.existsSync(sharedEnvPath)) loadEnv({ path: sharedEnvPath });
|
|
if (fs.existsSync(localEnvPath)) loadEnv({ path: localEnvPath });
|
|
|
|
if (!process.env.JWT_SECRET) {
|
|
console.error('JWT_SECRET is not set; refusing to start (per Phase 6.4)');
|
|
process.exit(1);
|
|
}
|
|
|
|
const app = express();
|
|
const port = Number(process.env.CHAT_PORT) || 3014;
|
|
|
|
console.log('Starting chat server with config:', {
|
|
host: process.env.CHAT_DB_HOST,
|
|
user: process.env.CHAT_DB_USER,
|
|
database: process.env.CHAT_DB_NAME || 'rocketchat_converted',
|
|
port: process.env.CHAT_DB_PORT,
|
|
chat_port: port,
|
|
});
|
|
|
|
// Rocket.Chat archive pool — routes.js reads it via global.pool.
|
|
const pool = new Pool({
|
|
host: process.env.CHAT_DB_HOST,
|
|
user: process.env.CHAT_DB_USER,
|
|
password: process.env.CHAT_DB_PASSWORD,
|
|
database: process.env.CHAT_DB_NAME || 'rocketchat_converted',
|
|
port: process.env.CHAT_DB_PORT,
|
|
});
|
|
global.pool = pool;
|
|
|
|
// inventory_db pool — used by authenticate() for user/permission lookups.
|
|
const inventoryPool = new Pool({
|
|
host: process.env.DB_HOST,
|
|
user: process.env.DB_USER,
|
|
password: process.env.DB_PASSWORD,
|
|
database: process.env.DB_NAME,
|
|
port: Number(process.env.DB_PORT) || 5432,
|
|
ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false,
|
|
});
|
|
|
|
app.use(requestLog());
|
|
app.use(express.json());
|
|
app.use(morgan('combined'));
|
|
app.use(cors(corsOptions));
|
|
|
|
// /health stays unauthenticated for out-of-band probes — mounted BEFORE
|
|
// authenticate() so monitoring tools on the host can poll without a JWT.
|
|
// Only reachable via localhost:3014 directly (Caddy routes /health to
|
|
// inventory-server:3010, not here).
|
|
app.get('/health', (req, res) => res.json({ status: 'healthy' }));
|
|
|
|
// Phase 9 §9.1 — per-server auth re-verification. Every chat route must pass
|
|
// authenticate() in addition to the Caddy forward_auth gate.
|
|
app.use(authenticate({ pool: inventoryPool, secret: process.env.JWT_SECRET }));
|
|
|
|
app.get('/test-db', async (req, res, next) => {
|
|
try {
|
|
const result = await pool.query('SELECT COUNT(*) as user_count FROM users WHERE active = true');
|
|
const messageResult = await pool.query('SELECT COUNT(*) as message_count FROM message');
|
|
const roomResult = await pool.query('SELECT COUNT(*) as room_count FROM room');
|
|
res.json({
|
|
status: 'success',
|
|
database: 'rocketchat_converted',
|
|
stats: {
|
|
active_users: parseInt(result.rows[0].user_count, 10),
|
|
total_messages: parseInt(messageResult.rows[0].message_count, 10),
|
|
total_rooms: parseInt(roomResult.rows[0].room_count, 10),
|
|
},
|
|
});
|
|
} catch (err) {
|
|
next(err);
|
|
}
|
|
});
|
|
|
|
app.use('/', chatRoutes);
|
|
|
|
app.use(errorHandler);
|
|
|
|
// Phase 9 §9.1 — bind to 127.0.0.1. Caddy reverse_proxy targets localhost:3014
|
|
// already; this closes the gap where unauthenticated direct-port access from
|
|
// any host on the network was possible.
|
|
const server = app.listen(port, '127.0.0.1', () => {
|
|
console.log(`Chat server running on 127.0.0.1:${port}`);
|
|
});
|
|
|
|
const shutdown = async (signal) => {
|
|
console.log(`chat-server shutting down (${signal})`);
|
|
server.close();
|
|
try { await pool.end(); } catch { /* ignore */ }
|
|
try { await inventoryPool.end(); } catch { /* ignore */ }
|
|
process.exit(0);
|
|
};
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|