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:
16
app.js
16
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) {
|
||||
<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>
|
||||
`;
|
||||
|
||||
12
migration_add_tracking.sql
Normal file
12
migration_add_tracking.sql
Normal 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
98
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -24,10 +24,11 @@
|
||||
"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"
|
||||
|
||||
37
server.js
37
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]
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user