From 61189ba66b48c144b31153b4a921938bac71cbc2 Mon Sep 17 00:00:00 2001 From: wartana Date: Mon, 19 Jan 2026 13:52:34 +0800 Subject: [PATCH] feat: Add IP address and browser tracking - Add database migration for ip_address and browser_info columns - Install ua-parser-js for browser detection - Add IP extraction helper function - Update POST /api/dokumentasi to capture client IP and browser - Update GET endpoints to return tracking data - Display IP and browser info in documentation detail view - Update server port to 3002 Features: - Automatic IP address capture from request headers - Browser and OS detection with version info - Display in detail modal for each documentation --- app.js | 16 ++++++- migration_add_tracking.sql | 12 +++++ package-lock.json | 98 ++++++++++++++++++++++++++++++++++++-- package.json | 7 +-- server.js | 37 +++++++++++--- 5 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 migration_add_tracking.sql 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] );