447 lines
13 KiB
JavaScript
447 lines
13 KiB
JavaScript
const express = require('express');
|
||
const { Pool } = require('pg');
|
||
const cors = require('cors');
|
||
const path = require('path');
|
||
require('dotenv').config();
|
||
|
||
const app = express();
|
||
const port = process.env.PORT || 3001;
|
||
|
||
app.use(cors());
|
||
app.use(express.json({ limit: '10mb' })); // support large payloads for base64 background images
|
||
|
||
// Database connection configuration
|
||
const isRenderInternal = process.env.DATABASE_URL && (process.env.DATABASE_URL.includes('.render.com') || process.env.DATABASE_URL.includes('@dpg-'));
|
||
const pool = process.env.DATABASE_URL ? new Pool({
|
||
connectionString: process.env.DATABASE_URL,
|
||
ssl: isRenderInternal
|
||
? false
|
||
: (process.env.DATABASE_URL.includes('sslmode=require')
|
||
? { rejectUnauthorized: false }
|
||
: (process.env.NODE_ENV === 'production' ? { rejectUnauthorized: false } : false))
|
||
}) : null;
|
||
|
||
// Default regions data
|
||
const DEFAULT_REGIONS = [
|
||
{
|
||
id: "assam",
|
||
name: "Assam",
|
||
address: "INDUSTRIES GROWTH CENTRE, MATIA (MORNAI)\nOPPOSITE TO SAINIK SCHOOL, GOALPARA, ASSAM - 783 101.",
|
||
phone: "",
|
||
},
|
||
{
|
||
id: "bhiwandi",
|
||
name: "Bhiwandi Plant",
|
||
address: "S.NO. 31/B / 32 A, DHAMANGAON OLD AGRA ROAD,\nTAL. - BHIWANDI, DISTT. - THANE, MUMBAI, MAHARASHTRA - 421 302.",
|
||
phone: "",
|
||
},
|
||
{
|
||
id: "erode",
|
||
name: "Erode",
|
||
address: "SF NO. 532, BHAVANI TO ANTHIYUR MAIN ROAD, MYLAMPADI,\nKANNADIPALAYAM POST, BHAVANI TK, ERODE DT - 638 314.",
|
||
phone: "04256 - 238 202 / 302 / 402",
|
||
},
|
||
{
|
||
id: "haridwar",
|
||
name: "Haridwar",
|
||
address: "PLOT NO.16 & 17,SECTOR-4, INTEGRATED INDUSTRIAL ESTATE,\nSIDCUL,RANIPUR, HARIDWAR-249403",
|
||
phone: "01334-239246",
|
||
},
|
||
{
|
||
id: "kanchipuram",
|
||
name: "Kanchipuram",
|
||
address: "NO. 18, ARYAPERUMBAKKAM VILLAGE, KANCHIPURAM - 631 502.",
|
||
phone: "2729 4580/81",
|
||
},
|
||
{
|
||
id: "pondy",
|
||
name: "Pondy",
|
||
address: "R.S. NO. 81/4, KORKADU VILLAGE, NETTAPAKKAM COMMUNE,\nPONDICHERRY -605 110.",
|
||
phone: "0413 - 2665146",
|
||
},
|
||
{
|
||
id: "ro-east",
|
||
name: "RO East",
|
||
address: "14TH FLOOR, P.S. SRIJAN TECH PARK, DN - 52, DN BLOCK, SECTOR - 5,\nSALT LAKE CITY, KOLKATA - 700091",
|
||
phone: "033 - 40669540",
|
||
},
|
||
{
|
||
id: "ro-north",
|
||
name: "RO North",
|
||
address: "D-12/ 2ND FLOOR, KAUSHAMBI, GHAZIABAD - 201010\nUTTAR PRADESH",
|
||
phone: "0120-4335396",
|
||
},
|
||
{
|
||
id: "ro-south",
|
||
name: "RO South",
|
||
address: "NO. 12, POONAMALLEE ROAD, EKKATTUTHANGAL,\nCHENNAI - 6OO 032.",
|
||
phone: "044 - 2225 1011 / 12",
|
||
},
|
||
{
|
||
id: "ro-west",
|
||
name: "RO West",
|
||
address: "504-A, RAHEJA PLAZA PREMISES CO-OP SOCIETY LTD.,\nL.B.S. MARG, GHATKOPAR (WEST), MUMBAI – 400086",
|
||
phone: "",
|
||
},
|
||
];
|
||
|
||
// In-memory fallback store for local development when DB is unreachable
|
||
const memoryStore = {
|
||
users: {
|
||
user: { password: "user", role: "user" },
|
||
admin: { password: "admin", role: "admin" }
|
||
},
|
||
regions: DEFAULT_REGIONS,
|
||
stats: {},
|
||
settings: {}
|
||
};
|
||
let dbConnected = false;
|
||
|
||
// Initialize Database Tables
|
||
async function initDb() {
|
||
if (!pool) {
|
||
dbConnected = false;
|
||
console.log("ℹ️ No DATABASE_URL set. Running in local memory mode.");
|
||
return;
|
||
}
|
||
let client;
|
||
try {
|
||
console.log("Connecting to PostgreSQL database...");
|
||
client = await pool.connect();
|
||
console.log("Initializing database tables...");
|
||
await client.query("BEGIN");
|
||
|
||
// 1. App Users Table
|
||
await client.query(`
|
||
CREATE TABLE IF NOT EXISTS app_users (
|
||
username VARCHAR(255) PRIMARY KEY,
|
||
password VARCHAR(255) NOT NULL,
|
||
role VARCHAR(50) NOT NULL
|
||
)
|
||
`);
|
||
|
||
// 2. Regions Table
|
||
await client.query(`
|
||
CREATE TABLE IF NOT EXISTS regions (
|
||
id VARCHAR(255) PRIMARY KEY,
|
||
name VARCHAR(255) NOT NULL,
|
||
address TEXT NOT NULL,
|
||
phone VARCHAR(255)
|
||
)
|
||
`);
|
||
|
||
// 3. Stats Table
|
||
await client.query(`
|
||
CREATE TABLE IF NOT EXISTS stats (
|
||
unit_id VARCHAR(255) PRIMARY KEY,
|
||
count INT NOT NULL DEFAULT 0
|
||
)
|
||
`);
|
||
|
||
// 4. Settings Table
|
||
await client.query(`
|
||
CREATE TABLE IF NOT EXISTS settings (
|
||
key VARCHAR(255) PRIMARY KEY,
|
||
value TEXT NOT NULL
|
||
)
|
||
`);
|
||
|
||
// Seed default users if empty
|
||
const usersCountRes = await client.query("SELECT COUNT(*) FROM app_users");
|
||
if (parseInt(usersCountRes.rows[0].count) === 0) {
|
||
console.log("Seeding default users...");
|
||
await client.query(`
|
||
INSERT INTO app_users (username, password, role) VALUES
|
||
('user', 'user', 'user'),
|
||
('admin', 'admin', 'admin')
|
||
`);
|
||
}
|
||
|
||
// Seed default regions if empty
|
||
const regionsCountRes = await client.query("SELECT COUNT(*) FROM regions");
|
||
if (parseInt(regionsCountRes.rows[0].count) === 0) {
|
||
console.log("Seeding default regions...");
|
||
for (const region of DEFAULT_REGIONS) {
|
||
await client.query(
|
||
"INSERT INTO regions (id, name, address, phone) VALUES ($1, $2, $3, $4)",
|
||
[region.id, region.name, region.address, region.phone]
|
||
);
|
||
}
|
||
}
|
||
|
||
await client.query("COMMIT");
|
||
dbConnected = true;
|
||
console.log("Database initialized successfully.");
|
||
} catch (err) {
|
||
if (client) {
|
||
try {
|
||
await client.query("ROLLBACK");
|
||
} catch (rollbackErr) {
|
||
console.error("Rollback failed:", rollbackErr);
|
||
}
|
||
}
|
||
dbConnected = false;
|
||
console.warn("⚠️ Could not connect to PostgreSQL DB (running in local fallback mode):", err.message);
|
||
} finally {
|
||
if (client) client.release();
|
||
}
|
||
}
|
||
|
||
// REST API Endpoints
|
||
|
||
// Authentication API
|
||
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" });
|
||
}
|
||
|
||
if (dbConnected) {
|
||
try {
|
||
const result = await pool.query(
|
||
"SELECT role FROM app_users WHERE username = $1 AND password = $2",
|
||
[username, password]
|
||
);
|
||
|
||
if (result.rows.length > 0) {
|
||
return res.json({ success: true, role: result.rows[0].role });
|
||
} else {
|
||
return res.status(401).json({ error: "Invalid username or password" });
|
||
}
|
||
} catch (err) {
|
||
console.error("DB Login error, using fallback:", err);
|
||
}
|
||
}
|
||
|
||
// Fallback mode login check
|
||
const fallbackUser = memoryStore.users[username];
|
||
if (fallbackUser && fallbackUser.password === password) {
|
||
return res.json({ success: true, role: fallbackUser.role });
|
||
} else {
|
||
return res.status(401).json({ error: "Invalid username or password" });
|
||
}
|
||
});
|
||
|
||
app.post('/api/auth/reset', async (req, res) => {
|
||
const { username, password } = req.body;
|
||
if (!username || !password) {
|
||
return res.status(400).json({ error: "Username and password are required" });
|
||
}
|
||
|
||
try {
|
||
const result = await pool.query(
|
||
"UPDATE app_users SET password = $2 WHERE username = $1 RETURNING role",
|
||
[username, password]
|
||
);
|
||
if (result.rows.length > 0) {
|
||
res.json({ success: true });
|
||
} else {
|
||
res.status(404).json({ error: "Username not found" });
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Database error during password reset" });
|
||
}
|
||
});
|
||
|
||
// Users Management APIs
|
||
app.get('/api/users', async (req, res) => {
|
||
if (dbConnected) {
|
||
try {
|
||
const result = await pool.query("SELECT username, role, password FROM app_users");
|
||
const usersMap = {};
|
||
result.rows.forEach(row => {
|
||
usersMap[row.username] = { password: row.password, role: row.role };
|
||
});
|
||
return res.json(usersMap);
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
return res.json(memoryStore.users);
|
||
});
|
||
|
||
app.post('/api/users', async (req, res) => {
|
||
const { username, password, role } = req.body;
|
||
if (!username || !password || !role) {
|
||
return res.status(400).json({ error: "Missing fields" });
|
||
}
|
||
|
||
if (dbConnected) {
|
||
try {
|
||
await pool.query(
|
||
"INSERT INTO app_users (username, password, role) VALUES ($1, $2, $3)",
|
||
[username, password, role]
|
||
);
|
||
return res.json({ success: true });
|
||
} catch (err) {
|
||
console.error(err);
|
||
if (err.code === '23505') {
|
||
return res.status(400).json({ error: "Username already exists" });
|
||
}
|
||
}
|
||
}
|
||
|
||
memoryStore.users[username] = { password, role };
|
||
return res.json({ success: true });
|
||
});
|
||
|
||
app.put('/api/users/:username', async (req, res) => {
|
||
const { username } = req.params;
|
||
const { password, role } = req.body;
|
||
if (!password || !role) {
|
||
return res.status(400).json({ error: "Missing fields" });
|
||
}
|
||
|
||
try {
|
||
const result = await pool.query(
|
||
"UPDATE app_users SET password = $1, role = $2 WHERE username = $3",
|
||
[password, role, username]
|
||
);
|
||
if (result.rowCount > 0) {
|
||
res.json({ success: true });
|
||
} else {
|
||
res.status(404).json({ error: "User not found" });
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to update user" });
|
||
}
|
||
});
|
||
|
||
app.delete('/api/users/:username', async (req, res) => {
|
||
const { username } = req.params;
|
||
try {
|
||
const result = await pool.query("DELETE FROM app_users WHERE username = $1", [username]);
|
||
if (result.rowCount > 0) {
|
||
res.json({ success: true });
|
||
} else {
|
||
res.status(404).json({ error: "User not found" });
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to delete user" });
|
||
}
|
||
});
|
||
|
||
// Regions Management APIs
|
||
app.get('/api/regions', async (req, res) => {
|
||
try {
|
||
const result = await pool.query("SELECT id, name, address, phone FROM regions ORDER BY name ASC");
|
||
res.json(result.rows);
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to fetch regions" });
|
||
}
|
||
});
|
||
|
||
app.post('/api/regions', async (req, res) => {
|
||
const regions = req.body;
|
||
if (!Array.isArray(regions)) {
|
||
return res.status(400).json({ error: "Invalid payload, expected array" });
|
||
}
|
||
|
||
const client = await pool.connect();
|
||
try {
|
||
await client.query("BEGIN");
|
||
await client.query("DELETE FROM regions");
|
||
for (const region of regions) {
|
||
await client.query(
|
||
"INSERT INTO regions (id, name, address, phone) VALUES ($1, $2, $3, $4)",
|
||
[region.id, region.name, region.address, region.phone || '']
|
||
);
|
||
}
|
||
await client.query("COMMIT");
|
||
res.json({ success: true });
|
||
} catch (err) {
|
||
await client.query("ROLLBACK");
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to save regions" });
|
||
} finally {
|
||
client.release();
|
||
}
|
||
});
|
||
|
||
// Stats Logging APIs
|
||
app.get('/api/stats', async (req, res) => {
|
||
try {
|
||
const result = await pool.query("SELECT unit_id, count FROM stats");
|
||
const statsMap = {};
|
||
result.rows.forEach(row => {
|
||
statsMap[row.unit_id] = row.count;
|
||
});
|
||
res.json(statsMap);
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to fetch stats" });
|
||
}
|
||
});
|
||
|
||
app.post('/api/stats', async (req, res) => {
|
||
const { unitId, count } = req.body;
|
||
if (!unitId || count === undefined) {
|
||
return res.status(400).json({ error: "Missing unitId or count" });
|
||
}
|
||
|
||
try {
|
||
await pool.query(
|
||
`INSERT INTO stats (unit_id, count) VALUES ($1, $2)
|
||
ON CONFLICT (unit_id) DO UPDATE SET count = stats.count + $2`,
|
||
[unitId, count]
|
||
);
|
||
res.json({ success: true });
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to log stats" });
|
||
}
|
||
});
|
||
|
||
// Settings APIs
|
||
app.get('/api/settings/:key', async (req, res) => {
|
||
const { key } = req.params;
|
||
try {
|
||
const result = await pool.query("SELECT value FROM settings WHERE key = $1", [key]);
|
||
if (result.rows.length > 0) {
|
||
res.json({ value: result.rows[0].value });
|
||
} else {
|
||
res.json({ value: null });
|
||
}
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to fetch settings" });
|
||
}
|
||
});
|
||
|
||
app.post('/api/settings', async (req, res) => {
|
||
const { key, value } = req.body;
|
||
if (!key || value === undefined) {
|
||
return res.status(400).json({ error: "Missing key or value" });
|
||
}
|
||
|
||
try {
|
||
await pool.query(
|
||
`INSERT INTO settings (key, value) VALUES ($1, $2)
|
||
ON CONFLICT (key) DO UPDATE SET value = $2`,
|
||
[key, value]
|
||
);
|
||
res.json({ success: true });
|
||
} catch (err) {
|
||
console.error(err);
|
||
res.status(500).json({ error: "Failed to update settings" });
|
||
}
|
||
});
|
||
|
||
// Serve Static Files in Production
|
||
app.use(express.static(path.join(__dirname, 'dist')));
|
||
|
||
// Fallback for Single Page Application routing (Vite React app)
|
||
app.get('*', (req, res) => {
|
||
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
|
||
});
|
||
|
||
// Start Server and Init Database
|
||
app.listen(port, async () => {
|
||
console.log(`Server is running on port ${port}`);
|
||
await initDb();
|
||
});
|