Initial Commit

This commit is contained in:
Sony Surahmn
2025-12-10 19:00:39 +07:00
commit e5baaf003a
12 changed files with 4043 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
node_modules/
dist/
logs/
*.log
.env
.DS_Store
*.swp
*.swo
*~

186
README.md Normal file
View File

@ -0,0 +1,186 @@
# SVC-HCM-CRAWLER
Hikvision Device Crawling Service for HCM Bridge
This service crawls Hikvision access control devices and sends events to the `svc-hcmbridge` service.
## Features
- Automatically crawls Hikvision devices configured for crawling (flag=1 or iscrawling=1)
- Fetches device MAC address from device info endpoint
- Handles HTTP Digest authentication
- Duplicate event detection
- Sends events to `svc-hcmbridge` at `/api/dooraccess/logs`
- PM2 deployment ready
## Prerequisites
- Node.js 20+
- MySQL database (same as svc-hcmbridge)
- PM2 (for deployment)
- `entity` package (from parent directory)
## Installation
1. Install dependencies:
```bash
npm install
```
2. Link the entity package:
```bash
cd ../entity
npm install
cd ../svc-hcm-crawler
```
3. Build the project:
```bash
npm run build
```
## Configuration
Edit `config/default.json` or `config/prod.json`:
```json
{
"database": {
"hcm": {
"host": "127.0.0.1",
"port": "3306",
"username": "root",
"password": "your_password",
"database": "tt_hcm"
}
},
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs"
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
},
"crawler": {
"interval": "*/1 * * * *"
}
}
```
## Development
Run in development mode:
```bash
npm run dev
```
## Deployment with PM2
1. Build the project:
```bash
npm run build
```
2. Start with PM2:
```bash
npm run pm2:start
```
Or manually:
```bash
pm2 start ecosystem.config.js
```
3. Check status:
```bash
pm2 list
pm2 logs svc-hcm-crawler
```
4. Stop the service:
```bash
npm run pm2:stop
# or
pm2 stop svc-hcm-crawler
```
5. Restart the service:
```bash
npm run pm2:restart
# or
pm2 restart svc-hcm-crawler
```
6. Delete from PM2:
```bash
npm run pm2:delete
# or
pm2 delete svc-hcm-crawler
```
## PM2 Commands
- `npm run pm2:start` - Start the service
- `npm run pm2:stop` - Stop the service
- `npm run pm2:restart` - Restart the service
- `npm run pm2:delete` - Delete from PM2
- `npm run pm2:logs` - View logs
## How It Works
1. The service runs on a cron schedule (default: every 1 minute)
2. It queries `tbl_deviceinfo` for devices with:
- `brand = 'HIKVISION'`
- `flag = 1` OR `iscrawling = 1`
- `isdeleted = 0`
3. For each device:
- Fetches MAC address from device info endpoint
- Makes authenticated requests to `/ISAPI/AccessControl/AcsEvent`
- Maps events to webhook format
- Checks for duplicates
- Sends events to `svc-hcmbridge` at `/api/dooraccess/logs`
## Logs
Logs are stored in:
- PM2 logs: `./logs/pm2-out.log` and `./logs/pm2-error.log`
- Console output (when running with `npm start`)
## Troubleshooting
1. **Database connection issues:**
- Check database credentials in config
- Ensure database is accessible
- Check if `entity` package is properly linked
2. **Bridge service not responding:**
- Ensure `svc-hcmbridge` is running
- Check bridge host/port in config
- Verify endpoint path is correct
3. **Device connection issues:**
- Check device IP addresses in `tbl_deviceinfo`
- Verify device credentials
- Check network connectivity to devices
4. **MAC address not being saved:**
- Check logs for XML parsing errors
- Verify device info endpoint is accessible
- Check device returns valid XML
## Environment Variables
Set `NODE_ENV` to `prod` for production:
```bash
export NODE_ENV=prod
```
Or use PM2 ecosystem config which sets it automatically.
## License
ISC

37
config/default.json Normal file
View File

@ -0,0 +1,37 @@
{
"app": {
"name": "SVC-HCM-CRAWLER",
"description": "Hikvision Device Crawling Service",
"version": "1.0.0",
"env": "Development"
},
"crawler": {
"interval": "*/1 * * * *",
"maxResults": 10,
"timeout": 10000
},
"database": {
"hcm": {
"engine": "mysql",
"host": "127.0.0.1",
"port": "3306",
"username": "root",
"password": "r00t@dm1n05",
"database": "tt_hcm",
"logging": false,
"sync": false
}
},
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
}
}

37
config/prod.json Normal file
View File

@ -0,0 +1,37 @@
{
"app": {
"name": "SVC-HCM-CRAWLER",
"description": "Hikvision Device Crawling Service",
"version": "1.0.0",
"env": "Production"
},
"crawler": {
"interval": "*/1 * * * *",
"maxResults": 10,
"timeout": 10000
},
"database": {
"hcm": {
"engine": "mysql",
"host": "127.0.0.1",
"port": "3306",
"username": "apphcm",
"password": "$ppHCMTT#2024",
"database": "dbhcm",
"logging": false,
"sync": false
}
},
"bridge": {
"host": "127.0.0.1",
"port": "3000",
"endpoint": "/api/dooraccess/logs",
"timeout": 30000
},
"hikvision": {
"defaultUsername": "admin",
"defaultPassword": "Passwordhik_1",
"defaultPort": 80
}
}

28
ecosystem.config.js Normal file
View File

@ -0,0 +1,28 @@
module.exports = {
apps: [
{
name: "svc-hcm-crawler",
script: "dist/src/index.js",
instances: 1,
exec_mode: "fork",
watch: false,
max_memory_restart: "500M",
error_file: "./logs/pm2-error.log",
out_file: "./logs/pm2-out.log",
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
merge_logs: true,
env: {
NODE_ENV: "default",
TZ: "Asia/Jakarta"
},
env_production: {
NODE_ENV: "prod",
TZ: "Asia/Jakarta"
},
autorestart: true,
max_restarts: 10,
min_uptime: "10s"
}
]
};

11
nodemon.json Normal file
View File

@ -0,0 +1,11 @@
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts", "node_modules"],
"exec": "ts-node src/index.ts",
"env": {
"NODE_ENV": "default",
"TZ": "Asia/Jakarta"
}
}

2314
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "svc-hcm-crawler",
"version": "1.0.0",
"description": "Hikvision Device Crawling Service for HCM Bridge",
"main": "dist/src/index.js",
"scripts": {
"start": "TZ='Asia/Jakarta' node dist/src/index.js",
"dev": "nodemon",
"build": "tsc",
"pm2:start": "pm2 start ecosystem.config.js",
"pm2:stop": "pm2 stop svc-hcm-crawler",
"pm2:restart": "pm2 restart svc-hcm-crawler",
"pm2:delete": "pm2 delete svc-hcm-crawler",
"pm2:logs": "pm2 logs svc-hcm-crawler"
},
"keywords": [
"hikvision",
"crawler",
"hcm"
],
"author": "STS",
"license": "ISC",
"dependencies": {
"@types/xml2js": "^0.4.14",
"axios": "^1.7.8",
"config": "^3.3.12",
"entity": "file:../entity",
"moment-timezone": "~0.5.46",
"mysql2": "^3.12.0",
"node-cron": "^4.1.0",
"reflect-metadata": "^0.2.2",
"tslog": "^4.9.3",
"typeorm": "^0.3.28",
"typescript": "^5.9.3",
"uuid": "^11.0.3",
"xml2js": "^0.6.2"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/node-cron": "^3.0.11",
"nodemon": "^3.0.0",
"ts-node": "^10.9.0"
}
}

44
src/helpers/orm.ts Normal file
View File

@ -0,0 +1,44 @@
import config from "config";
import { DataSource } from "typeorm";
import { entities } from "entity";
import { ILogObj, Logger } from "tslog";
export class OrmHelper {
static DB: DataSource = null;
static async setup(): Promise<void> {
const log: Logger<ILogObj> = new Logger({
name: "[OrmHelper]",
type: "pretty",
});
log.settings.prettyLogTimeZone = "local";
const engine: "mysql" = config.get("database.hcm.engine");
OrmHelper.DB = new DataSource({
type: engine,
host: config.get("database.hcm.host"),
port: Number(config.get("database.hcm.port")),
username: String(config.get("database.hcm.username")),
password: String(config.get("database.hcm.password")),
database: String(config.get("database.hcm.database")),
synchronize: config.get("database.hcm.sync"),
logging: config.get("database.hcm.logging"),
entities: entities,
subscribers: [],
migrations: [],
extra: {
query: "SET TIMEZONE='Asia/Jakarta';",
},
});
try {
await OrmHelper.DB.initialize();
log.info("Database initialized successfully");
} catch (error: any) {
log.error("Database initialization failed:", error);
throw error;
}
}
}

109
src/index.ts Normal file
View File

@ -0,0 +1,109 @@
import config from "config";
import moment from "moment-timezone";
import { ILogObj, Logger } from "tslog";
import cron from "node-cron";
import { OrmHelper } from "./helpers/orm";
import { CrawlingService } from "./services/crawling";
// Set timezone
moment.tz.setDefault("Asia/Jakarta");
const log: Logger<ILogObj> = new Logger({
name: "[CrawlerIndex]",
type: "pretty",
});
log.settings.prettyLogTimeZone = "local";
let isCrawlingRunning = false;
/**
* Start the crawling service
*/
async function startCrawlingService() {
try {
// Initialize database connection
log.info("Initializing database connection...");
await OrmHelper.setup();
// Wait a bit for database to be ready
await new Promise((resolve) => setTimeout(resolve, 2000));
// Verify database is initialized
if (!OrmHelper.DB || !OrmHelper.DB.isInitialized) {
throw new Error("Database not initialized");
}
log.info("Database connection established");
// Start cron job
const cronInterval = config.get("crawler.interval");
log.info(`Starting crawling service with interval: ${cronInterval}`);
const crawlingJob = cron.schedule(cronInterval, async () => {
if (isCrawlingRunning) {
log.warn("Crawling service is still running. Skipping this schedule.");
return;
}
log.info(`Running crawling service at: ${new Date().toLocaleString()}`);
isCrawlingRunning = true;
try {
await CrawlingService.runCrawling();
log.info(`Crawling service completed at: ${new Date().toLocaleString()}`);
} catch (error) {
log.error("Crawling service failed:", error);
} finally {
isCrawlingRunning = false;
}
});
log.info(
`Crawling service started. App: ${config.get("app.name")} v${config.get("app.version")}`
);
log.info(`Environment: ${config.get("app.env")}`);
log.info(`Cron schedule: ${cronInterval}`);
// Run immediately on startup (optional)
// Uncomment the line below if you want to run crawling immediately on startup
// await CrawlingService.runCrawling();
} catch (error) {
log.error("Failed to start crawling service:", error);
process.exit(1);
}
}
// Handle graceful shutdown
const onCloseSignal = () => {
log.info("SIGINT/SIGTERM received, shutting down gracefully");
if (OrmHelper.DB && OrmHelper.DB.isInitialized) {
OrmHelper.DB.destroy()
.then(() => {
log.info("Database connection closed");
process.exit(0);
})
.catch((error) => {
log.error("Error closing database connection:", error);
process.exit(1);
});
} else {
process.exit(0);
}
};
process.on("SIGINT", onCloseSignal);
process.on("SIGTERM", onCloseSignal);
// Handle uncaught errors
process.on("unhandledRejection", (reason, promise) => {
log.error("Unhandled Rejection at:", promise, "reason:", reason);
});
process.on("uncaughtException", (error) => {
log.error("Uncaught Exception:", error);
process.exit(1);
});
// Start the service
startCrawlingService();

1196
src/services/crawling.ts Normal file

File diff suppressed because it is too large Load Diff

26
tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": ["es6", "es2017", "esnext.asynciterable"],
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./",
"moduleResolution": "node",
"removeComments": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"resolveJsonModule": true,
"typeRoots": ["./node_modules/@types"],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"declarationMap": true,
"skipLibCheck": true
},
"include": ["./src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}