# Spesifikasi Pengambilan Data dari Alat Hikvision

Dokumen ini adalah kontrak teknis untuk membuat API server yang mengambil data **person** dan **event absensi** dari mesin Hikvision melalui ISAPI.

## Integrasi yang digunakan proyek PHP ini

Proyek PHP ini **tidak lagi memanggil alat secara langsung**. Person, event absensi, dan tes perangkat diteruskan ke API Node.js:

```text
URL_GATEWAY_DARI_ENV
```

Endpoint yang dipakai:

| Kebutuhan | Endpoint gateway | Metode |
| --- | --- | --- |
| Tes perangkat | `/api/hikvision/device` | POST |
| Seluruh person | `/api/hikvision/persons` | POST |
| Event absensi | `/api/hikvision/events` | POST |

Respons tes perangkat harus memuat `model`, `serialNumber`/`serial_number`, dan `firmwareVersion`/`firmware_version`; boleh berada langsung pada respons atau di dalam objek `device`, `deviceInfo`, atau `data`.

Setiap request menggunakan header berikut:

```http
Authorization: Bearer TOKEN_GATEWAY
Content-Type: application/json
Accept: application/json
```

Pada proyek PHP, alamat API dan Bearer token diatur pada **Pengaturan Perangkat ? API pengambilan data**. Token disimpan terenkripsi dan tidak ditampilkan kembali. Jangan masukkan token ke kode, dokumentasi publik, atau browser. Pengambilan person dan absensi dari menu, cron, serta sinkronisasi sesi otomatis menggunakan gateway ini.

## Koneksi alat

Server API harus dapat menjangkau alat melalui alamat berikut:

```text
{protocol}://{host}:{port}
```

Gunakan:

- HTTP Digest Authentication
- `username` dan `password` akun alat
- `Content-Type: application/json` untuk endpoint pencarian
- timeout koneksi dan total request yang dapat diatur
- jangan mengikuti redirect

Parameter koneksi yang diperlukan API:

```json
{
  "protocol": "https",
  "host": "IP_PUBLIK_ATAU_DNS_ALAT",
  "port": 443,
  "username": "USERNAME_ALAT",
  "password": "PASSWORD_ALAT",
  "timeout_seconds": 10
}
```

## Cek koneksi alat

Sebelum mengambil data, API dapat menguji alat:

```http
GET /ISAPI/System/deviceInfo
Authorization: Digest …
```

Respons yang diharapkan berbentuk XML `DeviceInfo`. Ambil bila tersedia:

- `model`
- `serialNumber`
- `firmwareVersion`

Respons HTTP selain `200` dianggap gagal.

## Ambil seluruh person

Endpoint alat:

```http
POST /ISAPI/AccessControl/UserInfo/Search?format=json
Content-Type: application/json
Authorization: Digest …
```

Body request:

```json
{
  "UserInfoSearchCond": {
    "searchID": "uuid-atau-id-acak-yang-sama-untuk-satu-pencarian",
    "searchResultPosition": 0,
    "maxResults": 30
  }
}
```

### Pagination

Ulangi request sampai semua hasil diterima:

1. Awali `searchResultPosition: 0`.
2. Baca `totalMatches`, `numOfMatches`, `responseStatusStrg`, dan `UserInfo`.
3. Tambahkan `searchResultPosition` dengan `numOfMatches`.
4. Lanjut bila posisi masih lebih kecil daripada `totalMatches`.
5. `responseStatusStrg` harus konsisten: `MORE` bila masih ada data, lalu `OK` atau `NO_MATCHES` setelah selesai.

Contoh respons yang diharapkan:

```json
{
  "UserInfoSearch": {
    "searchID": "...",
    "responseStatusStrg": "MORE",
    "totalMatches": 125,
    "numOfMatches": 30,
    "UserInfo": [
      {
        "employeeNo": "1001",
        "name": "Nama Person"
      }
    ]
  }
}
```

### Data keluaran API yang dibutuhkan proyek

```json
{
  "persons": [
    {
      "device_person_id": "1001",
      "person_name": "Nama Person"
    }
  ]
}
```

Aturan validasi:

- `employeeNo` wajib ada, berupa nilai skalar, tidak kosong, maksimal 100 karakter, dan unik dalam satu hasil sinkronisasi.
- `name` boleh kosong/null, maksimal 255 karakter bila ada.
- Jangan menyatakan data person valid bila struktur respons, jumlah halaman, atau total hasil tidak konsisten.
- Daftar kosong harus diperlakukan sebagai kondisi yang perlu diverifikasi, bukan otomatis menghapus data lokal.

## Ambil event absensi

Endpoint alat:

```http
POST /ISAPI/AccessControl/AcsEvent?format=json
Content-Type: application/json
Authorization: Digest …
```

Body request untuk rentang waktu:

```json
{
  "AcsEventCond": {
    "searchID": "uuid-atau-id-acak-yang-sama-untuk-satu-pencarian",
    "searchResultPosition": 0,
    "maxResults": 30,
    "major": 5,
    "minor": 0,
    "startTime": "2026-10-20T00:00:00+07:00",
    "endTime": "2026-10-20T23:59:59+07:00"
  }
}
```

Gunakan zona waktu `Asia/Jakarta` (`+07:00`) pada `startTime` dan `endTime`.

### Pagination event

Mekanismenya sama seperti person:

1. Awali posisi `0`.
2. Baca `totalMatches`, `numOfMatches`, `responseStatusStrg`, dan `InfoList` dari `AcsEvent`.
3. Tambah posisi sebesar `numOfMatches`.
4. Berhenti saat posisi sama dengan `totalMatches`.
5. Terima status `OK`, `MORE`, `NO_MATCHES`, atau `NO MATCH` sesuai firmware.
6. Jika posisi belum selesai tetapi `numOfMatches` bernilai `0`, anggap respons tidak valid agar tidak terjadi loop tanpa akhir.

Contoh respons:

```json
{
  "AcsEvent": {
    "searchID": "...",
    "responseStatusStrg": "OK",
    "totalMatches": 1,
    "numOfMatches": 1,
    "InfoList": [
      {
        "employeeNoString": "1001",
        "time": "2026-10-20T08:12:15+07:00",
        "serialNo": 12345,
        "major": 5,
        "minor": 0,
        "attendanceStatus": "checkIn"
      }
    ]
  }
}
```

### Data keluaran API yang dibutuhkan proyek

```json
{
  "events": [
    {
      "device_person_id": "1001",
      "event_time": "2026-10-20T08:12:15+07:00",
      "serial_no": "12345",
      "major": "5",
      "minor": "0",
      "attendance_status": "checkIn"
    }
  ]
}
```

Aturan validasi:

- `employeeNoString`, `time`, `serialNo`, `major`, dan `minor` wajib tersedia untuk event yang akan disimpan.
- Abaikan event dengan `employeeNoString` kosong atau bernilai `0`.
- Simpan waktu event dalam UTC di database; konversi dari waktu `+07:00` yang diberikan alat.
- Jangan duplikasi event. Kunci idempoten yang direkomendasikan:

```text
SHA-256(serialNo | event_time_UTC | employeeNoString | major | minor)
```

- Sinkronisasi rentang yang sama boleh dijalankan berulang; hanya event yang belum ada yang boleh ditambahkan.

## Referensi implementasi gateway Node.js + Next.js

Buat API ini dengan **Next.js App Router** dan TypeScript. Semua komunikasi ke alat harus berjalan di server melalui Route Handler atau modul server-only. Jangan panggil ISAPI langsung dari browser, karena kredensial alat tidak boleh terkirim ke client.

Struktur yang disarankan:

```text
app/
  api/
    hikvision/
      persons/route.ts
      events/route.ts
lib/
  hikvision/
    client.ts        # HTTP Digest dan request ISAPI
    persons.ts       # pagination + validasi person
    attendance.ts    # pagination + validasi event
    schemas.ts       # Zod schema request/respons
```

### Dependensi

Gunakan Node.js runtime, bukan Edge runtime, karena implementasi HTTP Digest dan modul Node diperlukan.

```bash
npm install digest-fetch zod
```

Contoh deklarasi route:

```ts
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
```

### Konfigurasi rahasia

Simpan kredensial di environment variable server. Jangan beri awalan `NEXT_PUBLIC_`.

```ini
HIKVISION_GATEWAY_URL=URL_GATEWAY_DARI_ENV
HIKVISION_GATEWAY_TOKEN=Bearer-token-server
```

Untuk konfigurasi multi-alat, simpan kredensial terenkripsi di database dan hanya dekripsi di server. Jangan kembalikan kata sandi dalam respons API atau log.

### Client ISAPI

Contoh dasar memakai `digest-fetch`:

```ts
// lib/hikvision/client.ts
import DigestFetch from 'digest-fetch';

const baseUrl = `${process.env.HIKVISION_PROTOCOL}://${process.env.HIKVISION_HOST}:${process.env.HIKVISION_PORT}`;

function client() {
  return new DigestFetch(
    process.env.HIKVISION_USERNAME!,
    process.env.HIKVISION_PASSWORD!,
  );
}

export async function isapiPost(path: string, body: unknown) {
  const response = await client().fetch(`${baseUrl}${path}`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(10_000),
    redirect: 'manual',
  });
  if (!response.ok) throw new Error(`ISAPI HTTP ${response.status}`);
  return response.json();
}
```

Tambahkan penanganan timeout, respons bukan JSON, dan error jaringan. Jangan memakai `redirect: 'follow'`.

### Route person

```text
POST /api/hikvision/persons
```

Route memanggil `fetchPersons()` yang mengulang endpoint `UserInfo/Search` sampai semua halaman diterima. Respons sukses API server:

```json
{
  "persons": [
    { "device_person_id": "1001", "person_name": "Nama Person" }
  ]
}
```

### Route event absensi

```text
POST /api/hikvision/events
```

Body API server:

```json
{
  "startTime": "2026-10-20T00:00:00+07:00",
  "endTime": "2026-10-20T23:59:59+07:00"
}
```

Validasi body dengan Zod sebelum diteruskan ke alat. Route memanggil `fetchAttendanceEvents(startTime, endTime)` dan mengembalikan:

```json
{
  "events": [
    {
      "device_person_id": "1001",
      "event_time": "2026-10-20T08:12:15+07:00",
      "serial_no": "12345",
      "major": "5",
      "minor": "0",
      "attendance_status": "checkIn"
    }
  ]
}
```

### Keamanan route Next.js

Route di atas tidak boleh terbuka tanpa autentikasi. Lindungi dengan session aplikasi, API key server-to-server, atau Bearer token yang disimpan sebagai environment variable. Batasi origin bila route memang diakses dari aplikasi lain.

Jangan kirim konfigurasi alat, respons mentah yang memuat data sensitif, atau detail error kredensial ke browser. Catat detail error hanya di log server.

## API server yang perlu dibuat

Agent dapat membuat dua operasi internal/server-side:

```text
fetchPersons(deviceConfig) -> { persons: [...] }
fetchAttendanceEvents(deviceConfig, startTime, endTime) -> { events: [...] }
```

Kedua operasi harus mengembalikan error terstruktur apabila terjadi timeout, koneksi gagal, HTTP bukan `200`, respons bukan JSON valid, struktur respons tidak sesuai, atau pagination tidak lengkap.

## Batasan firmware

Endpoint serta bentuk respons dapat berbeda antar firmware Hikvision. Implementasi pertama harus menggunakan endpoint di atas, lalu simpan respons gagal yang sudah disamarkan untuk penyesuaian apabila alat memberikan struktur berbeda.
