├── .env.example ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── index.js ├── install.ps1 ├── package.json ├── pnpm-lock.yaml └── words.txt /.env.example: -------------------------------------------------------------------------------- 1 | SERVER_PORT= 2 | FP_WIN_TITLE= 3 | FP_INS_PATH= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .env 4 | .env.* 5 | !.env.example 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 150 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Habib Mustofa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JKN Fingerprint Bot 2 | 3 | Solusi untuk mesin APM (Anjungan Pendaftaran Mandiri) yang berbasis web agar dapat membuka aplikasi sidik jadi BPJS Kesehatan melalui browser. 4 | 5 | https://github.com/ssecd/jkn-fp-bot/assets/25121822/a73610d6-95d6-4726-bb37-b639984b76f2 6 | 7 | ## Instalasi 8 | 9 | Sebelum memulai instalasi, buka **Windows Powershell** lalu jalankan perintah berikut: 10 | 11 | ```ps1 12 | Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy Unrestricted -Force; 13 | ``` 14 | 15 | Tujuannya untuk mengubah ExecutionPolicy supaya dapat menjalankan script instalasi. 16 | 17 | Clone repository ini atau download [Zip](https://github.com/mustofa-id/jkn-fp-bot/archive/refs/heads/main.zip) secara manual jika Git belum terpasang. Setelah clone atau download/extract Zip, klik kanan script `install.ps1` lalu pilih `Run with PowerShell`, jika terdapat prompt terkait Execution Policy, ketik huruf `A` yakni `Yes to All` lalu tunggu hingga proses instalasi selesai. Jika instalasi berhasil, server bot akan berjalan di port 3000 secara default dan seharusnya dapat di-akses melalui browser di alamat http://localhost:3000. 18 | 19 | ## Penggunaan 20 | 21 | Menggunakan `fetch` JavaScript 22 | 23 | ```js 24 | async function openFingerprint() { 25 | const response = await fetch(`http://localhost:3000`, { 26 | method: 'POST', 27 | body: new URLSearchParams({ 28 | username: 'username-fp', 29 | password: 'password-fp', 30 | card_number: 'no-kartu-bpjs', 31 | exit: true, // wait window for exit (optional, default false) 32 | wait: 2_000 // wait for login to completed (optional, default 3_593) 33 | }) 34 | }); 35 | 36 | if (response.ok) { 37 | // Response OK setelah jendela aplikasi sidik jari ditutup 38 | } else { 39 | const result = await response.json(); 40 | alert(result.message); 41 | } 42 | } 43 | ``` 44 | 45 | ## Konfigurasi 46 | 47 | Konfigurasi tersimpan pada file `.env`, beberapa konfigurasi tersedia diantaranya: 48 | 49 | - `SERVER_PORT` Port server default-nya `3000` 50 | - `FP_WIN_TITLE` Windows title aplikasi sidik jari BPJS default-nya `Aplikasi Registrasi Sidik Jari` 51 | - `FP_INS_PATH` Lokasi instalasi aplikasi sidik jari BPJS default-nya `C:\\Program Files (x86)\\BPJS Kesehatan\\Aplikasi Sidik Jari BPJS Kesehatan\\After.exe` 52 | 53 | Template file konfigurasi dapat di salin dari file [.env.example](./.env.example) 54 | 55 | ## Lisensi 56 | 57 | [MIT](./LICENSE) 58 | 59 | ## Lainnya 60 | 61 | - [Pemecahan Masalah](https://github.com/mustofa-id/jkn-fp-bot/issues?q=is%3Aissue) 62 | - [Laporkan Bug](https://github.com/mustofa-id/jkn-fp-bot/issues/new) 63 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import bot from 'node-autoit-koffi'; 3 | import { createServer } from 'node:http'; 4 | import qs from 'node:querystring'; 5 | import pkg from './package.json' assert { type: 'json' }; 6 | 7 | const host = `127.0.0.1`; // bind the server to the loopback interface so we don't expose it 8 | const port = Number(process.env.SERVER_PORT) || 3000; 9 | const fp_win_title = process.env.FP_WIN_TITLE || 'Aplikasi Registrasi Sidik Jari'; 10 | const fp_ins_path = process.env.FP_INS_PATH || 'C:\\Program Files (x86)\\BPJS Kesehatan\\Aplikasi Sidik Jari BPJS Kesehatan\\After.exe'; 11 | 12 | const server = createServer((req, res) => { 13 | // allow cors 14 | res.setHeader('Access-Control-Allow-Origin', '*'); 15 | res.setHeader('Access-Control-Allow-Methods', 'POST'); 16 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); 17 | 18 | /** @param {Error} error */ 19 | function handle_error(error) { 20 | console.error(error); 21 | json(500, { message: error?.message || `Internal server error` }); 22 | } 23 | 24 | /** 25 | * @param {number} status 26 | * @param {any =} data 27 | */ 28 | function json(status, data) { 29 | res.writeHead(status, { 'Content-Type': 'application/json' }); 30 | if (!data) return res.end(); 31 | res.end(JSON.stringify(data)); 32 | } 33 | 34 | try { 35 | const url = new URL(req.url || '/', `http://${host}`); 36 | if (url.pathname === '/' && req.method === 'GET') { 37 | // service info 38 | json(200, { message: pkg.description }); 39 | } else if (url.pathname === '/' && req.method === 'POST') { 40 | // apm bot service 41 | let body = ''; 42 | req.on('data', (chunk) => (body += chunk.toString())); 43 | req.on('end', () => { 44 | const form_data = qs.parse(body); 45 | const username = form_data['username']; 46 | const password = form_data['password']; 47 | const card_number = form_data['card_number']; 48 | const exit = form_data['exit'] === 'true'; 49 | const wait = form_data['wait']; 50 | 51 | if (!username || !password || !card_number) { 52 | return json(400, { 53 | message: `username, password, and card_number are required fields` 54 | }); 55 | } 56 | 57 | run_bot({ username, password, card_number, exit, wait }) 58 | .then(() => json(201)) 59 | .catch((e) => handle_error(e)); 60 | }); 61 | } else { 62 | json(404, { message: `Not found` }); 63 | } 64 | } catch (error) { 65 | handle_error(error); 66 | } 67 | }); 68 | 69 | server.on('error', (err) => { 70 | // might to try restarting the server or take other actions 71 | console.error('Server error:', err); 72 | }); 73 | 74 | server.listen(port, host, () => { 75 | console.log(`Server running at http://${host}:${port}`); 76 | }); 77 | 78 | /** @param {number} ms */ 79 | function delay(ms) { 80 | return new Promise((resolve) => setTimeout(resolve, ms)); 81 | } 82 | 83 | async function run_bot({ username, password, card_number, exit, wait }) { 84 | // open or activate the application window 85 | const already_open = await bot.winExists(fp_win_title); 86 | if (!already_open) { 87 | await bot.run(fp_ins_path); 88 | await bot.winWait(fp_win_title); // wait for the application window to appear 89 | } 90 | 91 | await bot.winActivate(fp_win_title); // activate the application window 92 | await bot.winWaitActive(fp_win_title); // wait for the application to be in focus 93 | 94 | if (exit) { 95 | await bot.winSetOnTop(fp_win_title, '', 1); // set window on top 96 | } 97 | 98 | // get the position and size of the window 99 | const win_pos = await bot.winGetPos(fp_win_title); 100 | if (!win_pos) throw new Error('Failed to get window position'); 101 | 102 | // use top and left positions to calculate absolute points 103 | const { top, left } = win_pos; 104 | 105 | // login if window just open up 106 | if (already_open) { 107 | // focus number input 108 | await bot.mouseMove(left + 223, top + 121, 0); 109 | await bot.mouseClick('left'); 110 | 111 | // clear number input 112 | await bot.send('^a'); 113 | await bot.send('{BACKSPACE}'); 114 | } else { 115 | // focus to the first input 116 | await bot.mouseMove(left + 223, top + 179, 0); 117 | await bot.mouseClick('left'); 118 | 119 | await delay(1000); 120 | 121 | // clear and enter the username 122 | await bot.send('^a'); 123 | await bot.send('{BACKSPACE}'); 124 | await bot.send(username); 125 | 126 | await bot.send('{TAB}'); 127 | 128 | // clear and enter the password 129 | await bot.send('^a'); 130 | await bot.send('{BACKSPACE}'); 131 | await bot.send(password); 132 | 133 | // hit enter key for login 134 | await bot.send('{ENTER}'); 135 | 136 | await delay(+wait || 3_593); 137 | } 138 | 139 | // send card number 140 | await bot.send(card_number); 141 | 142 | if (exit) { 143 | // wait for window to close 144 | await bot.winWaitClose(fp_win_title); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /install.ps1: -------------------------------------------------------------------------------- 1 | # Define the Node.js LTS version 2 | $nodeVersion = "20.14.0" 3 | $nodeLtsUrl64 = "https://nodejs.org/dist/v$nodeVersion/node-v$nodeVersion-x64.msi" 4 | $nodeLtsUrl32 = "https://nodejs.org/dist/v$nodeVersion/node-v$nodeVersion-x86.msi" 5 | 6 | # Determine if the system is 64-bit or 32-bit 7 | $is64Bit = [Environment]::Is64BitOperatingSystem 8 | $nodeLtsUrl = if ($is64Bit) { $nodeLtsUrl64 } else { $nodeLtsUrl32 } 9 | 10 | # Refresh env variables in the current session 11 | function Refresh-Env-Vars { 12 | $env:PATH = [System.Environment]::GetEnvironmentVariable("PATH", [System.EnvironmentVariableTarget]::Machine) 13 | } 14 | 15 | # Function to check if Node.js is installed 16 | function Check-Nodejs { 17 | try { 18 | node -v 19 | return $true 20 | } catch { 21 | return $false 22 | } 23 | } 24 | 25 | # Function to get the current Node.js version 26 | function Get-NodejsVersion { 27 | $version = node -v 2>$null 28 | if ($version) { 29 | return $version.TrimStart("v") 30 | } 31 | return $null 32 | } 33 | 34 | # Function to install Node.js 35 | function Install-Nodejs { 36 | $arch = if ($is64Bit) { 'x64' } else { 'x86' } 37 | $installerPath = "$env:TEMP\node-v$nodeVersion-$arch.msi" 38 | if (-Not (Test-Path $installerPath)) { 39 | Invoke-WebRequest -Uri $nodeLtsUrl -OutFile $installerPath 40 | } 41 | Start-Process msiexec.exe -ArgumentList "/i", $installerPath, "/passive", "/norestart" -Wait 42 | Refresh-Env-Vars 43 | } 44 | 45 | # Function to uninstall Node.js 46 | function Uninstall-Nodejs { 47 | $nodePath = (Get-Command node).Source 48 | $uninstallerPath = (Get-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like "Node.js*" }).UninstallString 49 | if ($uninstallerPath) { 50 | Start-Process "msiexec.exe" -ArgumentList "/x", $uninstallerPath, "/passive", "/norestart" -Wait 51 | } 52 | } 53 | 54 | # Check if Node.js is installed 55 | if (Check-Nodejs) { 56 | $currentVersion = Get-NodejsVersion 57 | if ($currentVersion -lt $nodeVersion) { 58 | Write-Output "Current Node.js version is $currentVersion. Upgrading to $nodeVersion." 59 | Uninstall-Nodejs 60 | Install-Nodejs 61 | } else { 62 | Write-Output "Node.js is already at version $currentVersion." 63 | } 64 | } else { 65 | Write-Output "Node.js is not installed. Installing version $nodeVersion." 66 | Install-Nodejs 67 | } 68 | 69 | # Install only production dependencies 70 | npm install --omit=dev 71 | 72 | # Install pm2 globally 73 | npm install pm2@latest -g 74 | 75 | Refresh-Env-Vars 76 | 77 | if (-not (Test-Path -Path .env)) { 78 | Move-Item -Path .env.example -Destination .env 79 | } 80 | 81 | # Run pm2 command 82 | pm2 start .\index.js --name jkn-fp-bot --node-args="--env-file=.env" 83 | 84 | # Save process list 85 | pm2 save --force 86 | 87 | # Create pm2-startup.bat file 88 | $batFilePath = "$env:TEMP\pm2-startup.bat" 89 | $batFileContent = "@echo off`npm2 resurrect`nexit" 90 | $batFileContent | Set-Content -Path $batFilePath 91 | 92 | # Move the batch file to the Startup folder 93 | $startupFolder = [System.Environment]::GetFolderPath('Startup') 94 | Move-Item -Path $batFilePath -Destination $startupFolder -Force 95 | 96 | Write-Output "Setup completed." 97 | Read-Host -Prompt "Press Enter to close this window" 98 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jkn-fp-api", 3 | "description": "JKN finger print API for automated APM", 4 | "version": "0.0.1", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node --env-file=.env --watch index.js" 9 | }, 10 | "keywords": [ 11 | "jkn", 12 | "bpjs", 13 | "bridging", 14 | "vclaim", 15 | "apm", 16 | "finger-print" 17 | ], 18 | "author": { 19 | "name": "Habib Mustofa", 20 | "email": "25121822+mustofa-id@users.noreply.github.com", 21 | "url": "https://github.com/mustofa-id" 22 | }, 23 | "license": "MIT", 24 | "dependencies": { 25 | "node-autoit-koffi": "^1.0.5" 26 | }, 27 | "devDependencies": { 28 | "@types/node": "^20.14.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | node-autoit-koffi: 12 | specifier: ^1.0.5 13 | version: 1.0.5 14 | devDependencies: 15 | '@types/node': 16 | specifier: ^20.14.5 17 | version: 20.14.5 18 | 19 | packages: 20 | 21 | '@types/node@20.14.5': 22 | resolution: {integrity: sha512-aoRR+fJkZT2l0aGOJhuA8frnCSoNX6W7U2mpNq63+BxBIj5BQFt8rHy627kijCmm63ijdSdwvGgpUsU6MBsZZA==} 23 | 24 | debug@4.3.5: 25 | resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==} 26 | engines: {node: '>=6.0'} 27 | peerDependencies: 28 | supports-color: '*' 29 | peerDependenciesMeta: 30 | supports-color: 31 | optional: true 32 | 33 | get-symbol-from-current-process-h@1.0.2: 34 | resolution: {integrity: sha512-syloC6fsCt62ELLrr1VKBM1ggOpMdetX9hTrdW77UQdcApPHLmf7CI7OKcN1c9kYuNxKcDe4iJ4FY9sX3aw2xw==} 35 | 36 | iconv-lite@0.6.3: 37 | resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 38 | engines: {node: '>=0.10.0'} 39 | 40 | koffi@2.8.9: 41 | resolution: {integrity: sha512-Q7ZKiWfPJo3UoaHG1oyWIkEzgaaf5A6HisFiBg9sfG+cTiID+9LzZBnsMzW7hBo5nZktXcHqHXE+4Liw3AU6dQ==} 42 | 43 | lodash@4.17.21: 44 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 45 | 46 | ms@2.1.2: 47 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 48 | 49 | node-addon-api@3.2.1: 50 | resolution: {integrity: sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==} 51 | 52 | node-autoit-koffi@1.0.5: 53 | resolution: {integrity: sha512-04oEUElh6TOxugwx5leLK8Z2g57mA65a3YeqINOT+zOL2IHGehM24oBmWE/ttEXVBOcGOViURCJNRO+tPnMV3Q==} 54 | 55 | node-gyp-build@4.8.1: 56 | resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} 57 | hasBin: true 58 | 59 | ref-napi@3.0.3: 60 | resolution: {integrity: sha512-LiMq/XDGcgodTYOMppikEtJelWsKQERbLQsYm0IOOnzhwE9xYZC7x8txNnFC9wJNOkPferQI4vD4ZkC0mDyrOA==} 61 | engines: {node: '>= 10.0'} 62 | 63 | safer-buffer@2.1.2: 64 | resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} 65 | 66 | undici-types@5.26.5: 67 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} 68 | 69 | snapshots: 70 | 71 | '@types/node@20.14.5': 72 | dependencies: 73 | undici-types: 5.26.5 74 | 75 | debug@4.3.5: 76 | dependencies: 77 | ms: 2.1.2 78 | 79 | get-symbol-from-current-process-h@1.0.2: {} 80 | 81 | iconv-lite@0.6.3: 82 | dependencies: 83 | safer-buffer: 2.1.2 84 | 85 | koffi@2.8.9: {} 86 | 87 | lodash@4.17.21: {} 88 | 89 | ms@2.1.2: {} 90 | 91 | node-addon-api@3.2.1: {} 92 | 93 | node-autoit-koffi@1.0.5: 94 | dependencies: 95 | iconv-lite: 0.6.3 96 | koffi: 2.8.9 97 | lodash: 4.17.21 98 | ref-napi: 3.0.3 99 | transitivePeerDependencies: 100 | - supports-color 101 | 102 | node-gyp-build@4.8.1: {} 103 | 104 | ref-napi@3.0.3: 105 | dependencies: 106 | debug: 4.3.5 107 | get-symbol-from-current-process-h: 1.0.2 108 | node-addon-api: 3.2.1 109 | node-gyp-build: 4.8.1 110 | transitivePeerDependencies: 111 | - supports-color 112 | 113 | safer-buffer@2.1.2: {} 114 | 115 | undici-types@5.26.5: {} 116 | -------------------------------------------------------------------------------- /words.txt: -------------------------------------------------------------------------------- 1 | autoit 2 | BPJS 3 | koffi 4 | vclaim 5 | --------------------------------------------------------------------------------