Initial Commit
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.DS_Store
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
186
README.md
Normal file
186
README.md
Normal 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
37
config/default.json
Normal 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
37
config/prod.json
Normal 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
28
ecosystem.config.js
Normal 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
11
nodemon.json
Normal 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
2314
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
45
package.json
Normal file
45
package.json
Normal 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
44
src/helpers/orm.ts
Normal 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
109
src/index.ts
Normal 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
1196
src/services/crawling.ts
Normal file
File diff suppressed because it is too large
Load Diff
26
tsconfig.json
Normal file
26
tsconfig.json
Normal 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"]
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user