diff --git a/app.js b/app.js
index 479ef99..02071f5 100644
--- a/app.js
+++ b/app.js
@@ -53,7 +53,7 @@ const elements = {
// API CONFIGURATION
// ============================================
-const API_BASE_URL = 'http://localhost:3000/api';
+const API_BASE_URL = 'http://localhost:3002/api';
async function checkApiConnection() {
try {
@@ -411,6 +411,20 @@ async function showDetail(id) {
Waktu Pengambilan Foto
${formatDateTime(data.timestamp)}
+
+ ${data.ipAddress ? `
+
+
IP Address
+
${escapeHtml(data.ipAddress)}
+
+ ` : ''}
+
+ ${data.browserInfo ? `
+
+
Browser / Perangkat
+
${escapeHtml(data.browserInfo)}
+
+ ` : ''}
`;
diff --git a/migration_add_tracking.sql b/migration_add_tracking.sql
new file mode 100644
index 0000000..696d980
--- /dev/null
+++ b/migration_add_tracking.sql
@@ -0,0 +1,12 @@
+-- Migration: Add IP Address and Browser Tracking
+-- Date: 2026-01-19
+
+USE dokumentasi;
+
+-- Add new columns to dokumentasi table
+ALTER TABLE dokumentasi
+ADD COLUMN ip_address VARCHAR(45) AFTER timestamp,
+ADD COLUMN browser_info VARCHAR(500) AFTER ip_address;
+
+-- Add index for IP address queries (optional)
+ALTER TABLE dokumentasi ADD INDEX idx_ip_address (ip_address);
diff --git a/package-lock.json b/package-lock.json
index 744103c..fff7297 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,19 @@
{
- "name": "dokumentasi-nasabah-koperasi",
+ "name": "dokumentasi-nasabah-lpd-gerana",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "dokumentasi-nasabah-koperasi",
+ "name": "dokumentasi-nasabah-lpd-gerana",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
- "mysql2": "^3.6.5"
+ "mysql2": "^3.6.5",
+ "ua-parser-js": "^2.0.8"
},
"devDependencies": {
"nodemon": "^3.0.2"
@@ -284,6 +285,26 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-europe-js": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/detect-europe-js/-/detect-europe-js-0.1.2.tgz",
+ "integrity": "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -700,6 +721,26 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
+ "node_modules/is-standalone-pwa": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/is-standalone-pwa/-/is-standalone-pwa-0.1.1.tgz",
+ "integrity": "sha512-9Cbovsa52vNQCjdXOzeQq5CnCbAcRk05aU62K20WO372NrTv0NxibLFCK6lQ4/iZEFdEA3p3t2VNOn8AJ53F5g==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/long": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -1319,6 +1360,57 @@
"node": ">= 0.6"
}
},
+ "node_modules/ua-is-frozen": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ua-is-frozen/-/ua-is-frozen-0.1.2.tgz",
+ "integrity": "sha512-RwKDW2p3iyWn4UbaxpP2+VxwqXh0jpvdxsYpZ5j/MLLiQOfbsV5shpgQiw93+KMYQPcteeMQ289MaAFzs3G9pw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/ua-parser-js": {
+ "version": "2.0.8",
+ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-2.0.8.tgz",
+ "integrity": "sha512-BdnBM5waFormdrOFBU+cA90R689V0tWUWlIG2i30UXxElHjuCu5+dOV2Etw3547jcQ/yaLtPm9wrqIuOY2bSJg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ua-parser-js"
+ },
+ {
+ "type": "paypal",
+ "url": "https://paypal.me/faisalman"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/faisalman"
+ }
+ ],
+ "license": "AGPL-3.0-or-later",
+ "dependencies": {
+ "detect-europe-js": "^0.1.2",
+ "is-standalone-pwa": "^0.1.1",
+ "ua-is-frozen": "^0.1.2"
+ },
+ "bin": {
+ "ua-parser-js": "script/cli.js"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
diff --git a/package.json b/package.json
index 2068b22..429d428 100644
--- a/package.json
+++ b/package.json
@@ -24,12 +24,13 @@
"author": "",
"license": "ISC",
"dependencies": {
+ "cors": "^2.8.5",
+ "dotenv": "^16.3.1",
"express": "^4.18.2",
"mysql2": "^3.6.5",
- "dotenv": "^16.3.1",
- "cors": "^2.8.5"
+ "ua-parser-js": "^2.0.8"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
-}
\ No newline at end of file
+}
diff --git a/server.js b/server.js
index 574d976..ce39bc0 100644
--- a/server.js
+++ b/server.js
@@ -2,6 +2,7 @@ require('dotenv').config();
const express = require('express');
const mysql = require('mysql2/promise');
const cors = require('cors');
+const UAParser = require('ua-parser-js');
const app = express();
const PORT = process.env.SERVER_PORT || 3000;
@@ -36,6 +37,26 @@ async function testConnection() {
}
}
+// ============================================
+// HELPER FUNCTIONS
+// ============================================
+
+// Get client IP address
+function getClientIp(req) {
+ return req.headers['x-forwarded-for']?.split(',')[0].trim() ||
+ req.headers['x-real-ip'] ||
+ req.socket.remoteAddress ||
+ req.connection.remoteAddress ||
+ 'unknown';
+}
+
+// Parse user agent
+function parseUserAgent(userAgentString) {
+ const parser = new UAParser(userAgentString);
+ const result = parser.getResult();
+ return `${result.browser.name || 'Unknown'} ${result.browser.version || ''} on ${result.os.name || 'Unknown'} ${result.os.version || ''}`;
+}
+
// ============================================
// API ENDPOINTS
// ============================================
@@ -58,16 +79,20 @@ app.post('/api/dokumentasi', async (req, res) => {
});
}
+ // Get IP and browser info
+ const ipAddress = getClientIp(req);
+ const browserInfo = parseUserAgent(req.headers['user-agent'] || '');
+
const [result] = await pool.execute(
- `INSERT INTO dokumentasi (nama, no_anggota, jenis_perjanjian, tanggal, catatan, foto)
- VALUES (?, ?, ?, ?, ?, ?)`,
- [nama, noAnggota, jenisPerjanjian, tanggal, catatan || null, foto]
+ `INSERT INTO dokumentasi (nama, no_anggota, jenis_perjanjian, tanggal, catatan, foto, ip_address, browser_info)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
+ [nama, noAnggota, jenisPerjanjian, tanggal, catatan || null, foto, ipAddress, browserInfo]
);
// Log activity
await pool.execute(
`INSERT INTO activity_log (action, dokumentasi_id, details) VALUES (?, ?, ?)`,
- ['CREATE', result.insertId, `Created documentation for ${nama}`]
+ ['CREATE', result.insertId, `Created documentation for ${nama} from ${ipAddress}`]
);
res.status(201).json({
@@ -89,7 +114,7 @@ app.get('/api/dokumentasi', async (req, res) => {
try {
const { search } = req.query;
- let query = 'SELECT id, nama, no_anggota as noAnggota, jenis_perjanjian as jenisPerjanjian, tanggal, catatan, foto, timestamp FROM dokumentasi';
+ let query = 'SELECT id, nama, no_anggota as noAnggota, jenis_perjanjian as jenisPerjanjian, tanggal, catatan, foto, timestamp, ip_address as ipAddress, browser_info as browserInfo FROM dokumentasi';
let params = [];
if (search) {
@@ -122,7 +147,7 @@ app.get('/api/dokumentasi/:id', async (req, res) => {
const [rows] = await pool.execute(
`SELECT id, nama, no_anggota as noAnggota, jenis_perjanjian as jenisPerjanjian,
- tanggal, catatan, foto, timestamp
+ tanggal, catatan, foto, timestamp, ip_address as ipAddress, browser_info as browserInfo
FROM dokumentasi WHERE id = ?`,
[id]
);