Phase 5 + all remaining
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
const { Client } = require('ssh2');
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
// Per Deviation #13 in CONSOLIDATION_PLAN.md: `ssh2` is CJS and its named export
|
||||
// (`Client`) isn't reliably detected by Node's CJS→ESM interop static analysis.
|
||||
// Default-import + destructure is the bulletproof pattern.
|
||||
import ssh2 from 'ssh2';
|
||||
import mysql from 'mysql2/promise';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const { Client } = ssh2;
|
||||
|
||||
// Connection pool configuration
|
||||
const connectionPool = {
|
||||
@@ -288,10 +293,10 @@ function getPoolStatus() {
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
export {
|
||||
getDbConnection,
|
||||
getCachedQuery,
|
||||
clearQueryCache,
|
||||
closeAllConnections,
|
||||
getPoolStatus
|
||||
getPoolStatus,
|
||||
};
|
||||
@@ -15,6 +15,7 @@
|
||||
"luxon": "^3.5.0",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql2": "^3.6.5",
|
||||
"pg": "^8.21.0",
|
||||
"ssh2": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1142,6 +1143,95 @@
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz",
|
||||
"integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.13.0",
|
||||
"pg-pool": "^3.14.0",
|
||||
"pg-protocol": "^1.14.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz",
|
||||
"integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz",
|
||||
"integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz",
|
||||
"integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz",
|
||||
"integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
@@ -1155,6 +1245,45 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -1416,6 +1545,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sqlstring": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
|
||||
@@ -1548,6 +1686,15 @@
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,24 @@
|
||||
"name": "acot-server",
|
||||
"version": "1.0.0",
|
||||
"description": "A Cherry On Top production database server",
|
||||
"type": "module",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"luxon": "^3.5.0",
|
||||
"morgan": "^1.10.0",
|
||||
"ssh2": "^1.14.0",
|
||||
"mysql2": "^3.6.5",
|
||||
"compression": "^1.7.4",
|
||||
"luxon": "^3.5.0"
|
||||
"pg": "^8.21.0",
|
||||
"ssh2": "^1.14.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,11 @@
|
||||
// NOTE: `users.phone` is not yet indexed in production. Admin will add
|
||||
// `idx_phone (phone)` — queries here assume that exists for acceptable latency.
|
||||
|
||||
const express = require('express');
|
||||
import express from 'express';
|
||||
import { getDbConnection, getCachedQuery } from '../db/connection.js';
|
||||
import { requirePhoneApiKey } from '../utils/phoneAuth.js';
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getCachedQuery } = require('../db/connection');
|
||||
const { requirePhoneApiKey } = require('../utils/phoneAuth');
|
||||
|
||||
// Order status labels mirror ACOTCustomerDataServiceProvider.php.
|
||||
const ORDER_STATUS_LABEL = {
|
||||
@@ -319,4 +320,4 @@ router.get('/:cid/orders', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
const { getDbConnection } = require('../db/connection');
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection } from '../db/connection.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -573,4 +573,4 @@ router.post('/simulate', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection, getPoolStatus } from '../db/connection.js';
|
||||
import { getTimeRangeConditions, _internal as timeHelpers } from '../utils/timeUtils.js';
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
const {
|
||||
getTimeRangeConditions,
|
||||
_internal: timeHelpers
|
||||
} = require('../utils/timeUtils');
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
|
||||
@@ -680,4 +677,4 @@ function getPreviousTimeRange(timeRange) {
|
||||
return map[timeRange] || timeRange;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
const {
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection, getPoolStatus } from '../db/connection.js';
|
||||
import {
|
||||
getTimeRangeConditions,
|
||||
formatBusinessDate,
|
||||
getBusinessDayBounds,
|
||||
_internal: timeHelpers
|
||||
} = require('../utils/timeUtils');
|
||||
_internal as timeHelpers,
|
||||
} from '../utils/timeUtils.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 1;
|
||||
@@ -1794,4 +1794,5 @@ router.get('/debug/pool', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection, getPoolStatus } from '../db/connection.js';
|
||||
import { getTimeRangeConditions } from '../utils/timeUtils.js';
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
const {
|
||||
getTimeRangeConditions,
|
||||
} = require('../utils/timeUtils');
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
|
||||
@@ -481,4 +479,4 @@ function getPreviousTimeRange(timeRange) {
|
||||
return map[timeRange] || timeRange;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
const express = require('express');
|
||||
const { DateTime } = require('luxon');
|
||||
import express from 'express';
|
||||
import { DateTime } from 'luxon';
|
||||
import { getDbConnection, getPoolStatus } from '../db/connection.js';
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getPoolStatus } = require('../db/connection');
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
|
||||
@@ -502,4 +502,4 @@ function isCurrentPayPeriod(payPeriod) {
|
||||
return now >= payPeriod.start && now <= payPeriod.end;
|
||||
}
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
import express from 'express';
|
||||
import { getDbConnection, getCachedQuery } from '../db/connection.js';
|
||||
|
||||
const router = express.Router();
|
||||
const { getDbConnection, getCachedQuery } = require('../db/connection');
|
||||
|
||||
// Test endpoint to count orders
|
||||
router.get('/order-count', async (req, res) => {
|
||||
@@ -54,4 +55,4 @@ router.get('/test-connection', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
export default router;
|
||||
@@ -1,103 +1,158 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const morgan = require('morgan');
|
||||
const compression = require('compression');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { closeAllConnections } = require('./db/connection');
|
||||
// acot-server — Phase 5 of CONSOLIDATION_PLAN.md.
|
||||
// Standalone service on ACOT_PORT (default 3012) exposing /api/acot/* against
|
||||
// the production MySQL `sg` database via an ssh2 tunnel (see db/connection.js).
|
||||
//
|
||||
// Auth model (two flavors, deliberate):
|
||||
// - /api/acot/customers/* : x-acot-api-key shared secret (used by acot-phone-server).
|
||||
// Mounted BEFORE authenticate() so its requirePhoneApiKey
|
||||
// path is the only gate.
|
||||
// - everything else : JWT Bearer via shared/auth/middleware.js authenticate().
|
||||
// Defense-in-depth on top of Caddy forward_auth.
|
||||
//
|
||||
// Shared infrastructure (Phase 2 + Phase 6):
|
||||
// - shared/auth/middleware.js authenticate() for SPA-served routes
|
||||
// - shared/cors/policy.js explicit allowed-origins list (Phase 6.6)
|
||||
// - shared/logging/request-log.js pino-http, Authorization/Cookie redacted (Phase 6.5/6.9)
|
||||
// - shared/errors/handler.js consistent error envelope, no leak in prod
|
||||
//
|
||||
// Env layering: /var/www/inventory/.env loaded FIRST (JWT_SECRET, DB_* for the
|
||||
// shared PG pool used by authenticate to look up user permissions). Local .env
|
||||
// loaded SECOND for ACOT-specific keys (PROD_DB_*, PROD_SSH_*, ACOT_PHONE_API_KEY).
|
||||
// dotenv defaults to override:false, so the first file wins on collisions.
|
||||
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
import express from 'express';
|
||||
import cors from 'cors';
|
||||
import compression from 'compression';
|
||||
import morgan from 'morgan';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import pg from 'pg';
|
||||
|
||||
import { authenticate } from '../../shared/auth/middleware.js';
|
||||
import { corsOptions } from '../../shared/cors/policy.js';
|
||||
import { errorHandler } from '../../shared/errors/handler.js';
|
||||
import { logger } from '../../shared/logging/logger.js';
|
||||
import { requestLog } from '../../shared/logging/request-log.js';
|
||||
|
||||
import { closeAllConnections } from './db/connection.js';
|
||||
|
||||
import testRouter from './routes/test.js';
|
||||
import eventsRouter from './routes/events.js';
|
||||
import discountsRouter from './routes/discounts.js';
|
||||
import employeeMetricsRouter from './routes/employee-metrics.js';
|
||||
import payrollMetricsRouter from './routes/payroll-metrics.js';
|
||||
import operationsMetricsRouter from './routes/operations-metrics.js';
|
||||
import customersRouter from './routes/customers.js';
|
||||
|
||||
const { Pool } = pg;
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// Layer envs: shared inventory .env first (JWT_SECRET, DB_*) then acot .env.
|
||||
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 });
|
||||
|
||||
// Phase 6.4 — refuse to start without JWT_SECRET. authenticate() would reject
|
||||
// every request anyway; failing fast surfaces the misconfiguration immediately.
|
||||
if (!process.env.JWT_SECRET) {
|
||||
logger.error('JWT_SECRET is not set; refusing to start (per Phase 6.4)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.ACOT_PORT || 3012;
|
||||
const PORT = Number(process.env.ACOT_PORT) || 3012;
|
||||
|
||||
// Create logs directory if it doesn't exist
|
||||
// Postgres pool for authenticate() (user/permission lookups against inventory_db).
|
||||
// All MySQL access goes through db/connection.js (separate, ssh-tunneled).
|
||||
const pool = 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,
|
||||
});
|
||||
|
||||
// Per-app access log on disk (kept from pre-conversion behavior; pino request-log
|
||||
// is mounted below for structured/redacted server-side logging).
|
||||
const logDir = path.join(__dirname, 'logs/app');
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
const accessLogStream = fs.createWriteStream(path.join(logDir, 'access.log'), { flags: 'a' });
|
||||
|
||||
// Create a write stream for access logs
|
||||
const accessLogStream = fs.createWriteStream(
|
||||
path.join(logDir, 'access.log'),
|
||||
{ flags: 'a' }
|
||||
);
|
||||
|
||||
// Middleware
|
||||
app.use(requestLog());
|
||||
app.use(compression());
|
||||
app.use(cors());
|
||||
app.use(cors(corsOptions));
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Logging middleware
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
app.use(morgan('combined', { stream: accessLogStream }));
|
||||
} else {
|
||||
app.use(morgan('dev'));
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'healthy',
|
||||
service: 'acot-server',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime()
|
||||
uptime: process.uptime(),
|
||||
});
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/acot/test', require('./routes/test'));
|
||||
app.use('/api/acot/events', require('./routes/events'));
|
||||
app.use('/api/acot/discounts', require('./routes/discounts'));
|
||||
app.use('/api/acot/employee-metrics', require('./routes/employee-metrics'));
|
||||
app.use('/api/acot/payroll-metrics', require('./routes/payroll-metrics'));
|
||||
app.use('/api/acot/operations-metrics', require('./routes/operations-metrics'));
|
||||
app.use('/api/acot/customers', require('./routes/customers'));
|
||||
// Customers route uses x-acot-api-key (shared secret with acot-phone-server),
|
||||
// NOT JWT — mount BEFORE authenticate() so requirePhoneApiKey is the only gate.
|
||||
app.use('/api/acot/customers', customersRouter);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Unhandled error:', err);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: process.env.NODE_ENV === 'production'
|
||||
? 'Internal server error'
|
||||
: err.message
|
||||
});
|
||||
});
|
||||
// All remaining /api/acot/* routes require a valid JWT.
|
||||
app.use('/api/acot', authenticate({ pool, secret: process.env.JWT_SECRET }));
|
||||
|
||||
// 404 handler
|
||||
app.use('/api/acot/test', testRouter);
|
||||
app.use('/api/acot/events', eventsRouter);
|
||||
app.use('/api/acot/discounts', discountsRouter);
|
||||
app.use('/api/acot/employee-metrics', employeeMetricsRouter);
|
||||
app.use('/api/acot/payroll-metrics', payrollMetricsRouter);
|
||||
app.use('/api/acot/operations-metrics', operationsMetricsRouter);
|
||||
|
||||
// 404 for unmatched /api routes (keeps prior behavior).
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Route not found'
|
||||
});
|
||||
res.status(404).json({ success: false, error: 'Route not found' });
|
||||
});
|
||||
|
||||
// Start server
|
||||
const server = app.listen(PORT, () => {
|
||||
console.log(`ACOT Server running on port ${PORT}`);
|
||||
console.log(`Environment: ${process.env.NODE_ENV}`);
|
||||
app.use(errorHandler);
|
||||
|
||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
||||
logger.info({ port: PORT, mode: process.env.NODE_ENV || 'development' }, 'acot-server listening');
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
const gracefulShutdown = async () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
const gracefulShutdown = async (signal) => {
|
||||
logger.info({ signal }, 'acot-server shutting down');
|
||||
server.close(async () => {
|
||||
console.log('HTTP server closed');
|
||||
|
||||
// Close database connections
|
||||
try {
|
||||
await closeAllConnections();
|
||||
console.log('Database connections closed');
|
||||
} catch (error) {
|
||||
console.error('Error closing database connections:', error);
|
||||
} catch (err) {
|
||||
logger.error({ err: { message: err.message } }, 'error closing MySQL pool');
|
||||
}
|
||||
|
||||
try {
|
||||
await pool.end();
|
||||
} catch { /* ignore */ }
|
||||
process.exit(0);
|
||||
});
|
||||
};
|
||||
|
||||
process.on('SIGTERM', gracefulShutdown);
|
||||
process.on('SIGINT', gracefulShutdown);
|
||||
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||
|
||||
module.exports = app;
|
||||
process.on('uncaughtException', (err) => {
|
||||
logger.error({ err: { message: err.message, stack: err.stack } }, 'uncaughtException');
|
||||
process.exit(1);
|
||||
});
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logger.error({ reason }, 'unhandledRejection');
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
// The acot-phone-server sends `x-acot-api-key` on every request; we compare
|
||||
// against ACOT_PHONE_API_KEY from the environment using timing-safe comparison.
|
||||
|
||||
const crypto = require('crypto');
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
function requirePhoneApiKey(req, res, next) {
|
||||
export function requirePhoneApiKey(req, res, next) {
|
||||
const expected = process.env.ACOT_PHONE_API_KEY;
|
||||
if (!expected) {
|
||||
console.error('ACOT_PHONE_API_KEY not configured; rejecting all requests');
|
||||
@@ -24,5 +24,3 @@ function requirePhoneApiKey(req, res, next) {
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { requirePhoneApiKey };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { DateTime } = require('luxon');
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const TIMEZONE = 'America/New_York';
|
||||
const DB_TIMEZONE = 'UTC-05:00';
|
||||
@@ -294,19 +294,24 @@ const formatMySQLDate = (input) => {
|
||||
return dt.setZone(DB_TIMEZONE).toFormat(DB_DATETIME_FORMAT);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
// Expose helpers for tests or advanced consumers.
|
||||
// Kept as a named `_internal` export so existing destructuring sites
|
||||
// (`const { _internal: timeHelpers } = require(...)` → ESM equivalent works)
|
||||
// don't need to change beyond the import-statement rewrite.
|
||||
const _internal = {
|
||||
getDayStart,
|
||||
getDayEnd,
|
||||
getWeekStart,
|
||||
getRangeForTimeRange,
|
||||
BUSINESS_DAY_START_HOUR,
|
||||
};
|
||||
|
||||
export {
|
||||
getBusinessDayBounds,
|
||||
getTimeRangeConditions,
|
||||
formatBusinessDate,
|
||||
getTimeRangeLabel,
|
||||
parseBusinessDate,
|
||||
formatMySQLDate,
|
||||
// Expose helpers for tests or advanced consumers
|
||||
_internal: {
|
||||
getDayStart,
|
||||
getDayEnd,
|
||||
getWeekStart,
|
||||
getRangeForTimeRange,
|
||||
BUSINESS_DAY_START_HOUR
|
||||
}
|
||||
_internal,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user