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
This commit is contained in:
2026-01-19 13:52:34 +08:00
parent d259088b71
commit 61189ba66b
5 changed files with 157 additions and 13 deletions

16
app.js
View File

@@ -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) {
<div class="detail-label">Waktu Pengambilan Foto</div>
<div class="detail-value">${formatDateTime(data.timestamp)}</div>
</div>
${data.ipAddress ? `
<div class="detail-row">
<div class="detail-label">IP Address</div>
<div class="detail-value">${escapeHtml(data.ipAddress)}</div>
</div>
` : ''}
${data.browserInfo ? `
<div class="detail-row">
<div class="detail-label">Browser / Perangkat</div>
<div class="detail-value">${escapeHtml(data.browserInfo)}</div>
</div>
` : ''}
</div>
</div>
`;

View File

@@ -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);

98
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}
}

View File

@@ -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]
);