From ce35d4d3f0e337a8a80f7c20385444c5e6b71a9d Mon Sep 17 00:00:00 2001 From: Danial HB Date: Sun, 14 Jun 2026 09:30:10 +0700 Subject: [PATCH] feat: unified portal server, single database migration & railway configuration --- .DS_Store | Bin 8196 -> 0 bytes .gitignore | 18 + .../package.json | 12 + .../server.js | 10 + .../.DS_Store | Bin 6148 -> 0 bytes .../node_modules/.DS_Store | Bin 20484 -> 0 bytes .../server.js | 42 +- WebGIS_PovertyMapping/server.js | 105 +-- WebGIS_SPBU_dan_LayerControl/.DS_Store | Bin 6148 -> 0 bytes WebGIS_SPBU_dan_LayerControl/index.html | 2 +- WebGIS_SPBU_dan_LayerControl/package.json | 5 +- WebGIS_SPBU_dan_LayerControl/server.js | 44 +- index.html | 2 +- package.json | 19 + server.js | 696 ++++++++++++++++++ 15 files changed, 884 insertions(+), 71 deletions(-) delete mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/package.json create mode 100644 WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/server.js delete mode 100644 WebGIS_ManajemenJalan_ManajemenHakBangunan/.DS_Store delete mode 100644 WebGIS_ManajemenJalan_ManajemenHakBangunan/node_modules/.DS_Store delete mode 100644 WebGIS_SPBU_dan_LayerControl/.DS_Store create mode 100644 package.json create mode 100644 server.js diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index ead970dbd514f553f9c2e3a1d504b03a9d0ae437..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMO=}ZD7=EXX?S@k6p@rOB@Fsyag`{Xf(llCI@B>#YMJ4VwAz8b-6Y{BqK)mW7 z@F)mg3Z6Xl*qc{D@Ti`=`5$~{W~14}9x9{?nSq&SGVeV5GS7VNyCEW$YZg;P2_iC( z1uo2?_(KwQkw?;0*m54oz$e-!iwd+!74k-U>jtfWRzNGD70?Q31^x#GaA%8ToN(_e zt1h(yT7ijFK%5UIvcPPJJ%#e`pAJP?uT(t-z=PB6eS{kQ2BY}X~rrw!mf-9-PY)2>$?k2~;mBn4uB8k{+=v$z5}nRkW;JQ9rqkV|*}0rbC(X4?rrV7hOBa{d z?-mX`Kj1HvFf2S=Xnshjm*ZE-E2pe|?pLMfnZw2Ld-QoeAI^($<}|1Z&|g#<3Smo#HzY9w8zBG2#}v(|M*3J*PQpPeSpZ{JT^T9_Yk z);&|_8O}Zz`j;c>x30cB9eYsI`3vvAK2@5Y!vCOBfA@sQ_}?C506I;O0@G@&2@(Ha zJNf(n1Sz72suh?#1z0R^<#QN!|MhoR0ugJQ$d8dl68(A#B?W~H$AK~&2mboQ5Oot> h1t%L~Pa#^6&iq3FZtnj3Q{?{B_rIJET}*}oKLIu4n0x>L diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..832bf2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Dependencies +node_modules/ +**/node_modules/ +package-lock.json + +# Environment variables / Secrets +.env +**/.env + +# OS Files +.DS_Store +**/.DS_Store +Thumbs.db + +# Build and logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/package.json b/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/package.json new file mode 100644 index 0000000..512873c --- /dev/null +++ b/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/package.json @@ -0,0 +1,12 @@ +{ + "name": "webgis_chloropleth", + "version": "1.0.0", + "description": "Static WebGIS Chloropleth app wrapped in an Express server", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^5.2.1" + } +} diff --git a/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/server.js b/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/server.js new file mode 100644 index 0000000..81bc29d --- /dev/null +++ b/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap/server.js @@ -0,0 +1,10 @@ +const express = require('express'); +const path = require('path'); +const app = express(); + +app.use(express.static(__dirname)); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`WebGIS Chloropleth static server running on port ${PORT}`); +}); diff --git a/WebGIS_ManajemenJalan_ManajemenHakBangunan/.DS_Store b/WebGIS_ManajemenJalan_ManajemenHakBangunan/.DS_Store deleted file mode 100644 index 208e5b06aa8916cc56faf7624c19b283356b0a00..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOG?B*5UtW#1avV=mwkn~K@4%@b~f&mF=1RJW?-0|U?0Le37*0Wcn4p7lnI6b zU5H2(bieBEs_M#vbaxRESG#6GG$*10O^`*I5SgA_I&$L~kTu3N^)ygJJL)C^{lzKC z{gm!C>z4NOZ)w}D7t7nB-)Q1j>xV^FCNxjeoi zbVx7|3JyKyzg;G1%NOpWLo=%!cMp?8OKB&L71K>+Xo3)SS39j6N6$2F45=+i))T z{{?=T$s&L35~E-s82D!l@T^|e6+X)E)-RtYcWpwuK@$4Zx&z}OpoByh4EzEE?~|`MX#fBK diff --git a/WebGIS_ManajemenJalan_ManajemenHakBangunan/node_modules/.DS_Store b/WebGIS_ManajemenJalan_ManajemenHakBangunan/node_modules/.DS_Store deleted file mode 100644 index e6bf3662a82255fbfecad4265f04cac675380938..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20484 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 AQ~&?~ diff --git a/WebGIS_ManajemenJalan_ManajemenHakBangunan/server.js b/WebGIS_ManajemenJalan_ManajemenHakBangunan/server.js index 3c3b254..ceb39b7 100644 --- a/WebGIS_ManajemenJalan_ManajemenHakBangunan/server.js +++ b/WebGIS_ManajemenJalan_ManajemenHakBangunan/server.js @@ -10,18 +10,38 @@ app.use(bodyParser.json()); app.use(express.static(__dirname)); // Serve static files from current directory // MySQL Connection -const db = mysql.createConnection({ - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || 'webgis_app', - port: process.env.DB_PORT || 3306 -}); +let db; +function handleDisconnect() { + const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL; + db = connectionString + ? mysql.createConnection(connectionString) + : mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'webgis_app', + port: process.env.DB_PORT || 3306 + }); -db.connect(err => { - if (err) throw err; - console.log('MySQL Connected...'); -}); + db.connect(err => { + if (err) { + console.error('MySQL connection failed: ' + err.stack); + setTimeout(handleDisconnect, 2000); + } else { + console.log('MySQL Connected...'); + } + }); + + db.on('error', err => { + console.error('Database error:', err); + if (err.code === 'PROTOCOL_CONNECTION_LOST') { + handleDisconnect(); + } else { + throw err; + } + }); +} +handleDisconnect(); // Route to save a Road app.post('/api/roads', (req, res) => { diff --git a/WebGIS_PovertyMapping/server.js b/WebGIS_PovertyMapping/server.js index b4a715d..d60de49 100644 --- a/WebGIS_PovertyMapping/server.js +++ b/WebGIS_PovertyMapping/server.js @@ -14,51 +14,72 @@ app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Database connection -const db = mysql.createConnection({ - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || 'webgis_donations', - port: process.env.DB_PORT || 3306 -}); - -db.connect(err => { - if (err) { - console.error('Database connection failed:', err); - return; - } - console.log('Connected to MySQL database.'); - - db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => { - if (err || results.length > 0) return; - console.log('Applying distribution_history schema migration...'); - const alterSql = ` - ALTER TABLE distribution_history - ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending', - ADD COLUMN verified_by int DEFAULT NULL, - ADD COLUMN verified_at datetime DEFAULT NULL, - ADD KEY verified_by (verified_by), - ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL - `; - db.query(alterSql, (alterErr) => { - if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr); - else console.log('distribution_history schema migrated successfully.'); +let db; +let migrationsRun = false; +function handleDisconnect() { + const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL; + db = connectionString + ? mysql.createConnection(connectionString) + : mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'webgis_donations', + port: process.env.DB_PORT || 3306 }); + + db.connect(err => { + if (err) { + console.error('Database connection failed:', err); + setTimeout(handleDisconnect, 2000); + return; + } + console.log('Connected to MySQL database.'); + + if (!migrationsRun) { + migrationsRun = true; + db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => { + if (err || results.length > 0) return; + console.log('Applying distribution_history schema migration...'); + const alterSql = ` + ALTER TABLE distribution_history + ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending', + ADD COLUMN verified_by int DEFAULT NULL, + ADD COLUMN verified_at datetime DEFAULT NULL, + ADD KEY verified_by (verified_by), + ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL + `; + db.query(alterSql, (alterErr) => { + if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr); + else console.log('distribution_history schema migrated successfully.'); + }); + }); + // Ensure religious_places has place_type column + db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => { + if (err2 || res2.length > 0) return; + console.log('Applying religious_places place_type migration...'); + const alterPlaces = ` + ALTER TABLE religious_places + ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque' + `; + db.query(alterPlaces, (aErr) => { + if (aErr) console.error('Failed to migrate religious_places schema:', aErr); + else console.log('religious_places schema migrated successfully.'); + }); + }); + } }); - // Ensure religious_places has place_type column - db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => { - if (err2 || res2.length > 0) return; - console.log('Applying religious_places place_type migration...'); - const alterPlaces = ` - ALTER TABLE religious_places - ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque' - `; - db.query(alterPlaces, (aErr) => { - if (aErr) console.error('Failed to migrate religious_places schema:', aErr); - else console.log('religious_places schema migrated successfully.'); - }); + + db.on('error', err => { + console.error('Database error:', err); + if (err.code === 'PROTOCOL_CONNECTION_LOST') { + handleDisconnect(); + } else { + throw err; + } }); -}); +} +handleDisconnect(); // --- MIDDLEWARES --- diff --git a/WebGIS_SPBU_dan_LayerControl/.DS_Store b/WebGIS_SPBU_dan_LayerControl/.DS_Store deleted file mode 100644 index 45b7090b857b2a576d17ce009892cbfbaec9942b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKyH3ME5S)b+k)TLPdB4CPoTBgrd;ka>D!@gQ-c`PfPh<8WgykU7(4bjqckb3Z zck&coF96#fhg)C+U`=<#$-~n8x%l17fy*mXFlje{S3G+GAZ!a3VZ?tj}~MA diff --git a/WebGIS_SPBU_dan_LayerControl/index.html b/WebGIS_SPBU_dan_LayerControl/index.html index 8fce824..e0630ae 100644 --- a/WebGIS_SPBU_dan_LayerControl/index.html +++ b/WebGIS_SPBU_dan_LayerControl/index.html @@ -224,7 +224,7 @@ // ---------------------------------------------------------------- // MARKERS STORE { id: { marker, spbu, group } } // ---------------------------------------------------------------- - const API_URL = 'http://localhost:3000/api/spbus'; + const API_URL = '/api/spbus'; let tempPopup = null; let markers = {}; diff --git a/WebGIS_SPBU_dan_LayerControl/package.json b/WebGIS_SPBU_dan_LayerControl/package.json index 7a96043..c158f27 100644 --- a/WebGIS_SPBU_dan_LayerControl/package.json +++ b/WebGIS_SPBU_dan_LayerControl/package.json @@ -2,9 +2,10 @@ "name": "webgis_gasstation", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "server.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js" }, "keywords": [], "author": "", diff --git a/WebGIS_SPBU_dan_LayerControl/server.js b/WebGIS_SPBU_dan_LayerControl/server.js index 0bffe66..336c514 100644 --- a/WebGIS_SPBU_dan_LayerControl/server.js +++ b/WebGIS_SPBU_dan_LayerControl/server.js @@ -11,22 +11,38 @@ app.use(bodyParser.json()); app.use(express.static(__dirname)); // Create MySQL connection -const db = mysql.createConnection({ - host: process.env.DB_HOST || 'localhost', - user: process.env.DB_USER || 'root', - password: process.env.DB_PASSWORD || '', - database: process.env.DB_NAME || 'spbu_gis', - port: process.env.DB_PORT || 3306 -}); +let db; +function handleDisconnect() { + const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL; + db = connectionString + ? mysql.createConnection(connectionString) + : mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'spbu_gis', + port: process.env.DB_PORT || 3306 + }); + db.connect(err => { + if (err) { + console.error('Database connection failed: ' + err.stack); + setTimeout(handleDisconnect, 2000); + } else { + console.log('Connected to MySQL database.'); + } + }); -db.connect(err => { - if (err) { - console.error('Database connection failed: ' + err.stack); - return; - } - console.log('Connected to MySQL database.'); -}); + db.on('error', err => { + console.error('Database error:', err); + if (err.code === 'PROTOCOL_CONNECTION_LOST') { + handleDisconnect(); + } else { + throw err; + } + }); +} +handleDisconnect(); // GET route to fetch all SPBU locations app.get('/api/spbus', (req, res) => { diff --git a/index.html b/index.html index 7021875..ce93307 100644 --- a/index.html +++ b/index.html @@ -271,7 +271,7 @@
- +
🛢️
SPBU & Kontrol Layer
diff --git a/package.json b/package.json new file mode 100644 index 0000000..9c2396a --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "webgis-portal-unified", + "version": "1.0.0", + "description": "Unified WebGIS portal combining SPBU, Road Management, Poverty Mapping, and Chloropleth apps", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js" + }, + "dependencies": { + "bcrypt": "^6.0.0", + "body-parser": "^2.2.2", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3", + "mysql2": "^3.22.3" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..ce6e7dc --- /dev/null +++ b/server.js @@ -0,0 +1,696 @@ +const express = require('express'); +const mysql = require('mysql2'); +const cors = require('cors'); +const bodyParser = require('body-parser'); +const path = require('path'); +const bcrypt = require('bcrypt'); +const jwt = require('jsonwebtoken'); +require('dotenv').config(); + +const app = express(); +app.use(cors()); +app.use(bodyParser.json()); +app.use(express.json()); + +// PORT setup +const PORT = process.env.PORT || 3000; + +// JWT SECRET setup +const JWT_SECRET = process.env.JWT_SECRET || 'super-secret-key-donations-webgis'; + +// Database connection & auto-reconnect logic +let db; +let migrationsRun = false; + +function handleDisconnect() { + const connectionString = process.env.DATABASE_URL || process.env.MYSQL_URL || process.env.MYSQL_PRIVATE_URL; + db = connectionString + ? mysql.createConnection(connectionString) + : mysql.createConnection({ + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'webgisDarin', + port: process.env.DB_PORT || 3306 + }); + + db.connect(err => { + if (err) { + console.error('Database connection failed:', err); + setTimeout(handleDisconnect, 2000); + return; + } + console.log('Connected to MySQL database (webgisDarin).'); + + // Poverty mapping migrations check + if (!migrationsRun) { + migrationsRun = true; + db.query("SHOW COLUMNS FROM distribution_history LIKE 'verification_status'", (err, results) => { + if (err || results.length > 0) return; + console.log('Applying distribution_history schema migration...'); + const alterSql = ` + ALTER TABLE distribution_history + ADD COLUMN verification_status enum('pending','verified','rejected') NOT NULL DEFAULT 'pending', + ADD COLUMN verified_by int DEFAULT NULL, + ADD COLUMN verified_at datetime DEFAULT NULL, + ADD KEY verified_by (verified_by), + ADD CONSTRAINT distribution_history_ibfk_4 FOREIGN KEY (verified_by) REFERENCES users (id) ON DELETE SET NULL + `; + db.query(alterSql, (alterErr) => { + if (alterErr) console.error('Failed to migrate distribution_history schema:', alterErr); + else console.log('distribution_history schema migrated successfully.'); + }); + }); + db.query("SHOW COLUMNS FROM religious_places LIKE 'place_type'", (err2, res2) => { + if (err2 || res2.length > 0) return; + console.log('Applying religious_places place_type migration...'); + const alterPlaces = ` + ALTER TABLE religious_places + ADD COLUMN place_type enum('mosque','church_catholic','church_protestant','pura','vihara','kelenteng') NOT NULL DEFAULT 'mosque' + `; + db.query(alterPlaces, (aErr) => { + if (aErr) console.error('Failed to migrate religious_places schema:', aErr); + else console.log('religious_places schema migrated successfully.'); + }); + }); + } + }); + + db.on('error', err => { + console.error('Database error:', err); + if (err.code === 'PROTOCOL_CONNECTION_LOST') { + handleDisconnect(); + } else { + throw err; + } + }); +} +handleDisconnect(); + +// ========================================== +// MIDDLEWARES (Poverty Mapping Auth) +// ========================================== +const authenticateJWT = (req, res, next) => { + const authHeader = req.headers.authorization; + if (authHeader) { + const token = authHeader.split(' ')[1]; + jwt.verify(token, JWT_SECRET, (err, user) => { + if (err) return res.sendStatus(403); + req.user = user; + next(); + }); + } else { + res.sendStatus(401); + } +}; + +const authorizeRoles = (...roles) => { + return (req, res, next) => { + if (!req.user || !roles.includes(req.user.role)) { + return res.status(403).json({ message: "Forbidden: insufficient permissions." }); + } + next(); + }; +}; + +const checkOwnership = (tableName) => { + return (req, res, next) => { + if (req.user.role === 'admin') return next(); + + let sql = `SELECT added_by FROM ${tableName} WHERE id = ?`; + if (tableName === 'households') { + sql = `SELECT surveyor_id AS added_by FROM poverty_assessments WHERE household_id = ? LIMIT 1`; + } + + db.query(sql, [req.params.id], (err, results) => { + if (err) return res.status(500).send(err); + if (results.length === 0) return res.status(404).send('Not Found'); + if (results[0].added_by !== req.user.id) return res.status(403).json({ message: "Forbidden: You did not add this entry." }); + next(); + }); + }; +}; + + +// ========================================== +// STATIC PORTAL & APP FILES MOUNTING +// ========================================== + +// Serve root portal landing page +app.use(express.static(__dirname)); + +// Serve sub-applications statically under their respective subpaths +app.use('/WebGIS_SPBU_dan_LayerControl', express.static(path.join(__dirname, 'WebGIS_SPBU_dan_LayerControl'))); +app.use('/WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap', express.static(path.join(__dirname, 'WebGIS_ChloroplethData_ImportGeoJSON_FromArcMap'))); +app.use('/WebGIS_ManajemenJalan_ManajemenHakBangunan', express.static(path.join(__dirname, 'WebGIS_ManajemenJalan_ManajemenHakBangunan'))); +app.use('/WebGIS_PovertyMapping', express.static(path.join(__dirname, 'WebGIS_PovertyMapping/public'))); + + +// ========================================== +// ROUTES - 1: SPBU App API +// ========================================== + +// GET route to fetch all SPBU locations +app.get('/api/spbus', (req, res) => { + db.query('SELECT * FROM locations', (err, results) => { + if (err) return res.status(500).send(err); + res.json(results); + }); +}); + +// POST route to save a new SPBU location +app.post('/api/spbus', (req, res) => { + const { name, open_window, spbu_number, lat, lng } = req.body; + const query = 'INSERT INTO locations (name, open_window, spbu_number, lat, lng) VALUES (?, ?, ?, ?, ?)'; + db.query(query, [name, open_window, spbu_number, lat, lng], (err, result) => { + if (err) return res.status(500).send(err); + res.json({ message: 'SPBU saved successfully!', id: result.insertId }); + }); +}); + +// PUT route to update an SPBU location +app.put('/api/spbus/:id', (req, res) => { + const { id } = req.params; + const { name, open_window, spbu_number, lat, lng } = req.body; + const query = 'UPDATE locations SET name = ?, open_window = ?, spbu_number = ?, lat = ?, lng = ? WHERE id = ?'; + db.query(query, [name, open_window, spbu_number, lat, lng, id], (err, result) => { + if (err) return res.status(500).send(err); + res.json({ message: 'SPBU updated successfully!' }); + }); +}); + +// DELETE route to delete an SPBU location +app.delete('/api/spbus/:id', (req, res) => { + const { id } = req.params; + const query = 'DELETE FROM locations WHERE id = ?'; + db.query(query, [id], (err, result) => { + if (err) return res.status(500).send(err); + res.json({ message: 'SPBU deleted successfully!' }); + }); +}); + + +// ========================================== +// ROUTES - 2: Manajemen Jalan & Hak Bangunan App API +// ========================================== + +// Route to save a Road +app.post('/api/roads', (req, res) => { + const { name, road_type, distance_meters, coordinates } = req.body; + const query = 'INSERT INTO roads (name, road_type, distance_meters, coordinates) VALUES (?, ?, ?, ?)'; + db.query(query, [name, road_type, distance_meters, JSON.stringify(coordinates)], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Road saved successfully!', id: result.insertId }); + }); +}); + +// Route to get all Roads +app.get('/api/roads', (req, res) => { + const query = 'SELECT * FROM roads'; + db.query(query, (err, results) => { + if (err) return res.status(500).send(err); + res.status(200).send(results); + }); +}); + +// Route to save Land +app.post('/api/lands', (req, res) => { + const { name, ownership_status, area_sq_meters, coordinates } = req.body; + const query = 'INSERT INTO lands (name, ownership_status, area_sq_meters, coordinates) VALUES (?, ?, ?, ?)'; + db.query(query, [name, ownership_status, area_sq_meters, JSON.stringify(coordinates)], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Land saved successfully!', id: result.insertId }); + }); +}); + +// Route to get all Lands +app.get('/api/lands', (req, res) => { + const query = 'SELECT * FROM lands'; + db.query(query, (err, results) => { + if (err) return res.status(500).send(err); + res.status(200).send(results); + }); +}); + +// Route to update a Road +app.put('/api/roads/:id', (req, res) => { + const { id } = req.params; + const { name, road_type, distance_meters, coordinates } = req.body; + const query = 'UPDATE roads SET name = ?, road_type = ?, distance_meters = ?, coordinates = ? WHERE id = ?'; + db.query(query, [name, road_type, distance_meters, JSON.stringify(coordinates), id], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Road updated successfully!' }); + }); +}); + +// Route to delete a Road +app.delete('/api/roads/:id', (req, res) => { + const { id } = req.params; + const query = 'DELETE FROM roads WHERE id = ?'; + db.query(query, [id], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Road deleted successfully!' }); + }); +}); + +// Route to update a Land +app.put('/api/lands/:id', (req, res) => { + const { id } = req.params; + const { name, ownership_status, area_sq_meters, coordinates } = req.body; + const query = 'UPDATE lands SET name = ?, ownership_status = ?, area_sq_meters = ?, coordinates = ? WHERE id = ?'; + db.query(query, [name, ownership_status, area_sq_meters, JSON.stringify(coordinates), id], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Land updated successfully!' }); + }); +}); + +// Route to delete a Land +app.delete('/api/lands/:id', (req, res) => { + const { id } = req.params; + const query = 'DELETE FROM lands WHERE id = ?'; + db.query(query, [id], (err, result) => { + if (err) return res.status(500).send(err); + res.status(200).send({ message: 'Land deleted successfully!' }); + }); +}); + + +// ========================================== +// ROUTES - 3: Poverty Mapping App API +// ========================================== + +// Initialize default admin if no admins exist +app.post('/api/auth/init', async (req, res) => { + try { + db.query("SELECT COUNT(*) as count FROM users WHERE role = 'admin'", async (err, results) => { + if (err) return res.status(500).send(err); + if (results[0].count > 0) return res.status(403).json({ message: 'Admin already initialized.' }); + + const hashedPassword = await bcrypt.hash('admin123', 10); + db.query("INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, 'admin')", + ['admin', 'admin@webgis.com', hashedPassword], (err) => { + if (err) return res.status(500).send(err); + res.json({ message: 'Default admin initialized. (username: admin, email: admin@webgis.com, pass: admin123)' }); + }); + }); + } catch (err) { + res.status(500).send(err); + } +}); + +// Register new users (Admins only) +app.post('/api/auth/register', authenticateJWT, authorizeRoles('admin'), async (req, res) => { + const { username, email, password, role } = req.body; + try { + const hashedPassword = await bcrypt.hash(password, 10); + db.query('INSERT INTO users (username, email, password_hash, role) VALUES (?, ?, ?, ?)', + [username, email, hashedPassword, role || 'surveyor'], (err, result) => { + if (err) { + if (err.code === 'ER_DUP_ENTRY') return res.status(400).json({ message: 'Username or email already exists' }); + return res.status(500).send(err); + } + res.status(201).json({ message: 'User created successfully', userId: result.insertId }); + }); + } catch (err) { + res.status(500).send(err); + } +}); + +// Login +app.post('/api/auth/login', (req, res) => { + const { email, password } = req.body; + db.query('SELECT * FROM users WHERE email = ?', [email], async (err, results) => { + if (err) return res.status(500).send(err); + if (results.length === 0) return res.status(401).json({ message: 'Invalid email or password' }); + + const user = results[0]; + const validPassword = await bcrypt.compare(password, user.password_hash); + if (!validPassword) return res.status(401).json({ message: 'Invalid email or password' }); + + const token = jwt.sign({ id: user.id, role: user.role, username: user.username }, JWT_SECRET, { expiresIn: '24h' }); + res.json({ message: 'Login successful', token, role: user.role, username: user.username }); + }); +}); + +// GET endpoints +app.get('/api/places', (req, res) => { + db.query('SELECT * FROM religious_places', (err, results) => { + if (err) res.status(500).send(err); + else res.json(results); + }); +}); + +app.get('/api/houses', (req, res) => { + const query = ` + SELECT + h.id, + h.kk_number, + h.head_nik, + h.head_name, + h.head_name AS name, + h.resident_count, + h.lat, + h.lng, + h.linked_place_id, + pa.id AS assessment_id, + pa.electricity_va, + pa.floor_material, + pa.wall_material, + pa.drinking_water_source, + pa.photo_evidence_url, + pa.verification_status, + pa.surveyor_id, + pa.verified_by + FROM households h + INNER JOIN poverty_assessments pa ON h.id = pa.household_id + `; + db.query(query, (err, results) => { + if (err) res.status(500).send(err); + else res.json(results); + }); +}); + +// POST endpoints +app.post('/api/places', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => { + const { name, lat, lng, radius, place_type } = req.body; + db.query('INSERT INTO religious_places (name, lat, lng, radius, place_type, added_by) VALUES (?, ?, ?, ?, ?, ?)', + [name, lat, lng, radius, place_type || 'mosque', req.user.id], (err, result) => { + if (err) res.status(500).send(err); + else res.json({ id: result.insertId, name, lat, lng, radius, place_type: place_type || 'mosque', added_by: req.user.id }); + }); +}); + +app.post('/api/houses', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => { + const { + kk_number, + head_nik, + head_name, + resident_count, + lat, + lng, + linked_place_id, + electricity_va, + floor_material, + wall_material, + drinking_water_source, + photo_evidence_url + } = req.body; + + db.beginTransaction(err => { + if (err) return res.status(500).send(err); + + const insertHousehold = ` + INSERT INTO households (kk_number, head_nik, head_name, resident_count, lat, lng, linked_place_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + `; + db.query(insertHousehold, [kk_number, head_nik, head_name, resident_count, lat, lng, linked_place_id], (err, result) => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + + const householdId = result.insertId; + const insertAssessment = ` + INSERT INTO poverty_assessments (household_id, surveyor_id, electricity_va, floor_material, wall_material, drinking_water_source, photo_evidence_url, verification_status) + VALUES (?, ?, ?, ?, ?, ?, ?, 'pending') + `; + db.query(insertAssessment, [householdId, req.user.id, electricity_va, floor_material, wall_material, drinking_water_source, photo_evidence_url || null], (err, resultAssess) => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + + db.commit(err => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + res.status(201).json({ + id: householdId, + kk_number, + head_nik, + head_name, + name: head_name, + resident_count: parseInt(resident_count), + lat: parseFloat(lat), + lng: parseFloat(lng), + linked_place_id, + assessment_id: resultAssess.insertId, + electricity_va, + floor_material, + wall_material, + drinking_water_source, + photo_evidence_url: photo_evidence_url || null, + verification_status: 'pending', + surveyor_id: req.user.id, + verified_by: null + }); + }); + }); + }); + }); +}); + +// PUT endpoints (Updating location via Drag) +app.put('/api/places/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('religious_places'), (req, res) => { + const { lat, lng } = req.body; + db.query('UPDATE religious_places SET lat=?, lng=? WHERE id=?', + [lat, lng, req.params.id], (err) => { + if (err) res.status(500).send(err); + else res.sendStatus(200); + }); +}); + +app.put('/api/houses/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('households'), (req, res) => { + const { lat, lng, linked_place_id } = req.body; + db.query('UPDATE households SET lat=?, lng=?, linked_place_id=? WHERE id=?', + [lat, lng, linked_place_id, req.params.id], (err) => { + if (err) res.status(500).send(err); + else res.sendStatus(200); + }); +}); + +// PUT endpoints (Updating Attributes) +app.put('/api/places/attr/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('religious_places'), (req, res) => { + const { name, radius, place_type } = req.body; + db.query('UPDATE religious_places SET name=?, radius=?, place_type=? WHERE id=?', + [name, radius, place_type || 'mosque', req.params.id], (err) => { + if (err) res.status(500).send(err); + else res.sendStatus(200); + }); +}); + +app.put('/api/houses/attr/:id', authenticateJWT, authorizeRoles('admin', 'surveyor'), checkOwnership('households'), (req, res) => { + const { + head_name, + kk_number, + head_nik, + resident_count, + electricity_va, + floor_material, + wall_material, + drinking_water_source + } = req.body; + + db.beginTransaction(err => { + if (err) return res.status(500).send(err); + + const updateHousehold = 'UPDATE households SET head_name=?, kk_number=?, head_nik=?, resident_count=? WHERE id=?'; + db.query(updateHousehold, [head_name, kk_number, head_nik, resident_count, req.params.id], (err) => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + + const updateAssessment = 'UPDATE poverty_assessments SET electricity_va=?, floor_material=?, wall_material=?, drinking_water_source=? WHERE household_id=?'; + db.query(updateAssessment, [electricity_va, floor_material, wall_material, drinking_water_source, req.params.id], (err) => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + + db.commit(err => { + if (err) { + return db.rollback(() => { + res.status(500).send(err); + }); + } + res.sendStatus(200); + }); + }); + }); + }); +}); + +app.put('/api/houses/verify/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => { + const verifySql = ` + UPDATE poverty_assessments + SET verification_status = 'verified', verified_by = ? + WHERE household_id = ? + `; + db.query(verifySql, [req.user.id, req.params.id], (err, result) => { + if (err) return res.status(500).json({ message: 'Database error', error: err.message }); + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Household assessment not found or already verified.' }); + } + res.json({ message: 'Household verified successfully.' }); + }); +}); + +// Admin: list households pending verification +app.get('/api/houses/pending', authenticateJWT, authorizeRoles('admin'), (req, res) => { + const sql = ` + SELECT pa.id AS assessment_id, h.id AS household_id, h.head_name, h.kk_number, h.head_nik, h.resident_count, + h.lat, h.lng, h.linked_place_id, pa.electricity_va, pa.floor_material, pa.wall_material, pa.drinking_water_source, pa.photo_evidence_url, pa.verification_status, pa.surveyor_id, u.username AS surveyor_name, rp.name AS place_name + FROM poverty_assessments pa + JOIN households h ON h.id = pa.household_id + LEFT JOIN users u ON u.id = pa.surveyor_id + LEFT JOIN religious_places rp ON rp.id = h.linked_place_id + WHERE pa.verification_status = 'pending' + ORDER BY pa.id DESC + `; + db.query(sql, (err, results) => { + if (err) return res.status(500).json({ message: 'Database error', error: err.message }); + res.json(results); + }); +}); + +// Analytics & logistics +app.get('/api/analytics/sembako-demand', authenticateJWT, (req, res) => { + const sql = ` + SELECT + rp.id AS place_id, + rp.name AS place_name, + rp.place_type AS place_type, + COUNT(DISTINCT h.id) AS households_needing_aid, + SUM(h.resident_count) AS total_residents_needing_aid + FROM religious_places rp + JOIN households h + ON h.linked_place_id = rp.id + JOIN poverty_assessments pa + ON pa.household_id = h.id + AND pa.verification_status = 'verified' + AND ( + pa.electricity_va IN ('none', '450') + OR pa.floor_material IN ('dirt', 'bamboo') + ) + WHERE NOT EXISTS ( + SELECT 1 FROM distribution_history dh + WHERE dh.house_id = h.id + AND dh.aid_category = 'sembako' + AND dh.verification_status = 'verified' + ) + GROUP BY rp.id, rp.name + ORDER BY households_needing_aid DESC + `; + db.query(sql, (err, results) => { + if (err) return res.status(500).json({ message: 'Database error', error: err.message }); + res.json(results); + }); +}); + +app.post('/api/distribution/log', authenticateJWT, authorizeRoles('admin', 'surveyor'), (req, res) => { + const { household_id, place_id, amount_value, notes } = req.body; + + if (!household_id) { + return res.status(400).json({ message: 'household_id is required.' }); + } + + const sql = ` + INSERT INTO distribution_history + (house_id, place_id, distributed_by, aid_category, amount_value, notes, verification_status) + VALUES (?, ?, ?, 'sembako', ?, ?, 'pending') + `; + db.query( + sql, + [ + household_id, + place_id || null, + req.user.id, + amount_value || 1, + notes || null + ], + (err, result) => { + if (err) return res.status(500).json({ message: 'Failed to log distribution.', error: err.message }); + res.status(201).json({ + message: 'Sembako distribution logged successfully.', + distribution_id: result.insertId + }); + } + ); +}); + +app.get('/api/distribution/pending', authenticateJWT, authorizeRoles('admin'), (req, res) => { + const sql = ` + SELECT + dh.id, + dh.house_id, + dh.place_id, + dh.distributed_by, + dh.amount_value, + dh.notes, + dh.verification_status, + dh.verified_by, + dh.verified_at, + h.head_name, + h.kk_number, + h.head_nik, + h.resident_count, + r.name AS place_name, + u.username AS distributed_by_name + FROM distribution_history dh + LEFT JOIN households h ON h.id = dh.house_id + LEFT JOIN religious_places r ON r.id = dh.place_id + LEFT JOIN users u ON u.id = dh.distributed_by + WHERE dh.aid_category = 'sembako' + AND dh.verification_status = 'pending' + ORDER BY dh.id DESC + `; + db.query(sql, (err, results) => { + if (err) return res.status(500).json({ message: 'Database error', error: err.message }); + res.json(results); + }); +}); + +app.put('/api/distribution/verify/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => { + const sql = ` + UPDATE distribution_history + SET verification_status = 'verified', verified_by = ?, verified_at = NOW() + WHERE id = ? + AND aid_category = 'sembako' + AND verification_status = 'pending' + `; + db.query(sql, [req.user.id, req.params.id], (err, result) => { + if (err) return res.status(500).json({ message: 'Database error', error: err.message }); + if (result.affectedRows === 0) { + return res.status(404).json({ message: 'Pending handover not found or already verified.' }); + } + res.json({ message: 'Handover verified successfully.' }); + }); +}); + +// DELETE endpoints +app.delete('/api/places/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => { + db.query('DELETE FROM religious_places WHERE id=?', [req.params.id], (err) => { + if (err) res.status(500).send(err); + else res.sendStatus(200); + }); +}); + +app.delete('/api/houses/:id', authenticateJWT, authorizeRoles('admin'), (req, res) => { + db.query('DELETE FROM households WHERE id=?', [req.params.id], (err) => { + if (err) res.status(500).send(err); + else res.sendStatus(200); + }); +}); + + +// Start the server +app.listen(PORT, () => { + console.log(`Unified WebGIS Portal server running on port ${PORT}`); +});