antigravity-tc 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.id.md +21 -2
- package/README.md +21 -2
- package/bin/restart.js +133 -0
- package/build.md +2 -0
- package/package.json +5 -3
- package/server.js +175 -37
- package/web/dist/admin.html +4 -4
- package/web/dist/assets/admin-C6nRfkO4.css +1 -0
- package/web/dist/assets/admin-CysSxUuD.js +3 -0
- package/web/dist/assets/index-9jTcCVxG.js +1 -0
- package/web/dist/assets/index-tUrCVzDv.css +1 -0
- package/web/dist/assets/styles-akdfcIjf.css +1 -0
- package/web/dist/index.html +4 -4
- package/web/dist/assets/admin-Bq8RntBN.js +0 -3
- package/web/dist/assets/admin-D4NgzEjp.css +0 -1
- package/web/dist/assets/index-BrxQZ67R.css +0 -1
- package/web/dist/assets/index-tJW-b1mM.js +0 -1
- package/web/dist/assets/styles-v8yOxg-B.css +0 -1
- /package/web/dist/assets/{styles-DllO4xsB.js → styles-BYH11bD8.js} +0 -0
package/README.id.md
CHANGED
|
@@ -52,9 +52,11 @@ Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di m
|
|
|
52
52
|
| --- | --- |
|
|
53
53
|
| Halaman donasi publik | Login Google + tempel kode, token dicek eligibility lalu disimpan |
|
|
54
54
|
| Portal Settings | Atur layout dan kata-kata halaman depan; data disimpan di aplikasi, bukan DB 9Router |
|
|
55
|
-
| Multi bahasa |
|
|
55
|
+
| Multi bahasa | UI dashboard dan portal multibahasa (Bahasa Indonesia & English) |
|
|
56
56
|
| Generate link personal | `?for=Nama` untuk sapaan otomatis |
|
|
57
|
-
| Dashboard admin | Statistik, tabel live, search, filter, export/import CSV |
|
|
57
|
+
| Dashboard admin | Statistik, tabel live, search, filter status, sumber, dan tier, export/import CSV |
|
|
58
|
+
| Deteksi & Filter Tier | Menampilkan tier donasi (**Free**, **Plus**, **Pro**, **Ultra**) serta filter cepat di tabel |
|
|
59
|
+
| Export Go Keyring | Salin token donatur format `go-keyring-base64` untuk login paksa ke Antigravity / `antigravity-cli` |
|
|
58
60
|
| Sync ke 9Router | Upsert semua donation logs ke providerConnections 9Router |
|
|
59
61
|
| Sync dari 9Router | Ambil providerConnections 9Router ke donation logs lokal |
|
|
60
62
|
| Check ALL Eligibility | Cek semua row bertoken dengan jeda aman dan rekap status |
|
|
@@ -64,6 +66,23 @@ Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di m
|
|
|
64
66
|
| Auto start | Berjalan otomatis mode tray saat OS login |
|
|
65
67
|
| Keamanan | Login JWT, secret unik per instalasi, tunnel terkunci selama password default |
|
|
66
68
|
|
|
69
|
+
## 🚀 Login ke Official Apps (Antigravity & antigravity-cli)
|
|
70
|
+
|
|
71
|
+
Anda dapat menggunakan token donatur untuk langsung login ke aplikasi resmi **Antigravity** atau **antigravity-cli** tanpa perlu autentikasi ulang via browser:
|
|
72
|
+
|
|
73
|
+
1. Di tabel donasi admin, klik tombol aksi (**...**) pada baris donatur → pilih **Go Keyring**.
|
|
74
|
+
2. Salin string `go-keyring-base64:...` atau langsung gunakan perintah terminal yang disediakan:
|
|
75
|
+
- **macOS (Keychain)**:
|
|
76
|
+
```bash
|
|
77
|
+
security add-generic-password -s gemini -a antigravity -w "<STRING_KEYRING>" -U
|
|
78
|
+
```
|
|
79
|
+
- **Windows (CMD / PowerShell)**:
|
|
80
|
+
```powershell
|
|
81
|
+
cmdkey /generic:"gemini:antigravity" /user:"antigravity" /pass:"<STRING_KEYRING>"
|
|
82
|
+
```
|
|
83
|
+
*(Atau isi manual via **Windows Credential Manager** → Generic Credentials).*
|
|
84
|
+
3. Buka Antigravity atau jalankan `antigravity-cli` — sesi login donatur langsung aktif.
|
|
85
|
+
|
|
67
86
|
## 📁 Lokasi Data
|
|
68
87
|
|
|
69
88
|
### Database 9Router
|
package/README.md
CHANGED
|
@@ -53,9 +53,11 @@ The dashboard opens at `http://localhost:3456/dashboard`, and the tray icon appe
|
|
|
53
53
|
| --- | --- |
|
|
54
54
|
| Public donation portal | Google login + manual code paste flow for donors |
|
|
55
55
|
| Portal Settings | Customize layout and front-page wording; stored in ATC app config, not 9Router DB |
|
|
56
|
-
| Multilingual
|
|
56
|
+
| Multilingual support | Fully localized dashboard and portal in English and Bahasa Indonesia |
|
|
57
57
|
| Personal donation links | `?for=Name` automatically customizes the greeting |
|
|
58
|
-
| Admin dashboard | Stats, live table, search, status/source filters, CSV import/export |
|
|
58
|
+
| Admin dashboard | Stats, live table, search, status/source/tier filters, CSV import/export |
|
|
59
|
+
| Tier Detection & Filter | Displays donor tier (**Free**, **Plus**, **Pro**, **Ultra**) with quick filters |
|
|
60
|
+
| Go Keyring Export | Export donor token as `go-keyring-base64` to force login into official Antigravity apps |
|
|
59
61
|
| Sync to 9Router | Upsert local donation logs into 9Router `providerConnections` |
|
|
60
62
|
| Sync from 9Router | Import 9Router `providerConnections` back into local donation logs |
|
|
61
63
|
| Check ALL Eligibility | Check all token rows with per-row delay and status recap |
|
|
@@ -65,6 +67,23 @@ The dashboard opens at `http://localhost:3456/dashboard`, and the tray icon appe
|
|
|
65
67
|
| Auto-start | Launches in tray mode when the OS starts |
|
|
66
68
|
| Security | JWT admin login, per-install secret, tunnel blocked while default password is active |
|
|
67
69
|
|
|
70
|
+
## 🚀 Log in to Official Apps (Antigravity & antigravity-cli)
|
|
71
|
+
|
|
72
|
+
You can use donated tokens to authenticate directly into the official **Antigravity** app or **antigravity-cli** without going through browser OAuth:
|
|
73
|
+
|
|
74
|
+
1. In the admin donation table, click the action menu (**...**) for any donor → select **Go Keyring**.
|
|
75
|
+
2. Copy the `go-keyring-base64:...` string or use the provided one-click terminal commands:
|
|
76
|
+
- **macOS (Keychain)**:
|
|
77
|
+
```bash
|
|
78
|
+
security add-generic-password -s gemini -a antigravity -w "<KEYRING_STRING>" -U
|
|
79
|
+
```
|
|
80
|
+
- **Windows (CMD / PowerShell)**:
|
|
81
|
+
```powershell
|
|
82
|
+
cmdkey /generic:"gemini:antigravity" /user:"antigravity" /pass:"<KEYRING_STRING>"
|
|
83
|
+
```
|
|
84
|
+
*(Or manually add under **Windows Credential Manager** → Generic Credentials).*
|
|
85
|
+
3. Launch Antigravity or execute `antigravity-cli` — the donor session will be active immediately.
|
|
86
|
+
|
|
68
87
|
## 📁 Data Locations
|
|
69
88
|
|
|
70
89
|
### 9Router database
|
package/bin/restart.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync, spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const { ensureEnvFile } = require('../src/paths');
|
|
8
|
+
require('dotenv').config({ path: ensureEnvFile(), quiet: true });
|
|
9
|
+
const { getConfig } = require('../src/config');
|
|
10
|
+
|
|
11
|
+
const config = getConfig();
|
|
12
|
+
const port = Number(process.env.PORT || config.get('port') || 3456);
|
|
13
|
+
const LOCK_FILE_PATH = path.join(os.tmpdir(), 'antigravity-token-collector.lock');
|
|
14
|
+
|
|
15
|
+
function isPidAlive(pid) {
|
|
16
|
+
try {
|
|
17
|
+
process.kill(pid, 0);
|
|
18
|
+
return true;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function findRunningPids() {
|
|
25
|
+
const pids = new Set();
|
|
26
|
+
|
|
27
|
+
// 1. Cek via lsof (port listener)
|
|
28
|
+
if (process.platform !== 'win32') {
|
|
29
|
+
try {
|
|
30
|
+
const lsofOut = execSync(`lsof -ti :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
31
|
+
if (lsofOut) {
|
|
32
|
+
for (const line of lsofOut.split('\n')) {
|
|
33
|
+
const pid = parseInt(line.trim(), 10);
|
|
34
|
+
if (pid && pid !== process.pid) pids.add(pid);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {}
|
|
38
|
+
|
|
39
|
+
// 2. Cek via ps untuk proses node yang menjalankan index.js
|
|
40
|
+
try {
|
|
41
|
+
const psOut = execSync('ps -eo pid,command', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
42
|
+
for (const line of psOut.split('\n')) {
|
|
43
|
+
if (line.includes('index.js') && (line.includes('antigravity-tc') || line.includes('antigravity-token-collector'))) {
|
|
44
|
+
const match = line.trim().match(/^(\d+)/);
|
|
45
|
+
if (match) {
|
|
46
|
+
const pid = parseInt(match[1], 10);
|
|
47
|
+
if (pid && pid !== process.pid) pids.add(pid);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {}
|
|
52
|
+
} else {
|
|
53
|
+
// Windows fallback
|
|
54
|
+
try {
|
|
55
|
+
const netstatOut = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
56
|
+
for (const line of netstatOut.split('\n')) {
|
|
57
|
+
const parts = line.trim().split(/\s+/);
|
|
58
|
+
const pid = parseInt(parts[parts.length - 1], 10);
|
|
59
|
+
if (pid && pid !== process.pid) pids.add(pid);
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return Array.from(pids);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function stopRunningInstances() {
|
|
68
|
+
const pids = findRunningPids();
|
|
69
|
+
if (!pids.length) {
|
|
70
|
+
console.log('ℹ️ Tidak ada instance Antigravity-tc yang sedang aktif.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(`🔄 Menghentikan instance Antigravity-tc yang sedang berjalan (PID: ${pids.join(', ')})...`);
|
|
75
|
+
|
|
76
|
+
for (const pid of pids) {
|
|
77
|
+
try {
|
|
78
|
+
process.kill(pid, 'SIGTERM');
|
|
79
|
+
} catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Tunggu hingga proses benar-benar berhenti
|
|
83
|
+
const deadline = Date.now() + 2500;
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
const alive = pids.filter(isPidAlive);
|
|
86
|
+
if (!alive.length) break;
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Force kill jika masih hidup
|
|
91
|
+
for (const pid of pids) {
|
|
92
|
+
if (isPidAlive(pid)) {
|
|
93
|
+
try {
|
|
94
|
+
process.kill(pid, 'SIGKILL');
|
|
95
|
+
} catch (e) {}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Bersihkan stale lock file bila ada
|
|
100
|
+
try {
|
|
101
|
+
if (fs.existsSync(LOCK_FILE_PATH)) fs.unlinkSync(LOCK_FILE_PATH);
|
|
102
|
+
const lockDir = `${LOCK_FILE_PATH}.lock`;
|
|
103
|
+
if (fs.existsSync(lockDir)) fs.rmSync(lockDir, { recursive: true, force: true });
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
|
|
106
|
+
console.log('✅ Instance lama berhasil dihentikan.');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
await stopRunningInstances();
|
|
111
|
+
|
|
112
|
+
// Beri jeda sejenak agar OS melepaskan socket port sepenuhnya
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
114
|
+
|
|
115
|
+
console.log('🚀 Memulai ulang Antigravity Token Collector...');
|
|
116
|
+
|
|
117
|
+
const indexPath = path.join(__dirname, '..', 'index.js');
|
|
118
|
+
const args = process.argv.slice(2);
|
|
119
|
+
const child = spawn(process.execPath, [indexPath, ...args], {
|
|
120
|
+
cwd: path.join(__dirname, '..'),
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
env: process.env,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
child.on('exit', (code) => {
|
|
126
|
+
process.exit(code || 0);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main().catch((err) => {
|
|
131
|
+
console.error('❌ Gagal merestart aplikasi:', err);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
});
|
package/build.md
CHANGED
|
@@ -35,6 +35,8 @@ web/dist/
|
|
|
35
35
|
npm version major # 1.0.0 → 2.0.0 (breaking change)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
> **Tips**: Jika git working tree belum di-commit, gunakan flag `--no-git-tag-version`, misal: `npm version 1.1.4 --no-git-tag-version`.
|
|
39
|
+
|
|
38
40
|
3. **Lint bersih**: `npm run lint`
|
|
39
41
|
4. **Verifikasi isi paket** (yang penting — pastikan secret tidak ikut):
|
|
40
42
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "antigravity-tc",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Antigravity Token Collector — tray app untuk mengumpulkan & mengelola donasi token Antigravity ke 9Router",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"antigravity-tc": "./index.js",
|
|
8
|
-
"antigravity-tc-reset-password": "./bin/reset-password.js"
|
|
8
|
+
"antigravity-tc-reset-password": "./bin/reset-password.js",
|
|
9
|
+
"antigravity-tc-restart": "./bin/restart.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"index.js",
|
|
@@ -24,7 +25,8 @@
|
|
|
24
25
|
"build:web": "vite build --config web/vite.config.mjs",
|
|
25
26
|
"lint": "eslint .",
|
|
26
27
|
"prepublishOnly": "npm run lint && npm run build:web",
|
|
27
|
-
"start": "node index.js"
|
|
28
|
+
"start": "node index.js",
|
|
29
|
+
"restart": "node bin/restart.js"
|
|
28
30
|
},
|
|
29
31
|
"keywords": [
|
|
30
32
|
"antigravity",
|
package/server.js
CHANGED
|
@@ -166,7 +166,9 @@ function initDonationLogDb() {
|
|
|
166
166
|
['eligibility_status', 'TEXT'],
|
|
167
167
|
['eligibility_message', 'TEXT'],
|
|
168
168
|
['eligibility_details', 'TEXT'],
|
|
169
|
-
['eligibility_checked_at', 'TEXT']
|
|
169
|
+
['eligibility_checked_at', 'TEXT'],
|
|
170
|
+
['tier', 'TEXT'],
|
|
171
|
+
['id_token', 'TEXT']
|
|
170
172
|
];
|
|
171
173
|
for (const [name, type] of additions) {
|
|
172
174
|
if (!columns.includes(name)) db.exec(`ALTER TABLE donation_logs ADD COLUMN ${name} ${type}`);
|
|
@@ -184,6 +186,7 @@ function writeDonationLog({
|
|
|
184
186
|
accessToken,
|
|
185
187
|
refreshToken,
|
|
186
188
|
expiry,
|
|
189
|
+
idToken,
|
|
187
190
|
}) {
|
|
188
191
|
const now = new Date().toISOString();
|
|
189
192
|
const normalizedEmail = String(donorEmail || '').trim().toLowerCase();
|
|
@@ -197,7 +200,9 @@ function writeDonationLog({
|
|
|
197
200
|
db.prepare(`
|
|
198
201
|
UPDATE donation_logs SET
|
|
199
202
|
donor_name = ?, donor_email = ?, requested_for = ?, source = ?, status = ?, message = ?,
|
|
200
|
-
access_token = ?, refresh_token = ?, expiry = ?,
|
|
203
|
+
access_token = ?, refresh_token = ?, expiry = ?,
|
|
204
|
+
id_token = COALESCE(?, id_token),
|
|
205
|
+
created_at = ?, updated_at = ?
|
|
201
206
|
WHERE id = ?
|
|
202
207
|
`).run(
|
|
203
208
|
donorName,
|
|
@@ -209,6 +214,7 @@ function writeDonationLog({
|
|
|
209
214
|
accessToken || '',
|
|
210
215
|
refreshToken || '',
|
|
211
216
|
expiry || '',
|
|
217
|
+
idToken || null,
|
|
212
218
|
now,
|
|
213
219
|
now,
|
|
214
220
|
existing.id
|
|
@@ -219,8 +225,8 @@ function writeDonationLog({
|
|
|
219
225
|
db.prepare(`
|
|
220
226
|
INSERT INTO donation_logs (
|
|
221
227
|
id, donor_name, donor_email, requested_for, source, status, message,
|
|
222
|
-
access_token, refresh_token, expiry, created_at, updated_at
|
|
223
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
228
|
+
access_token, refresh_token, expiry, id_token, created_at, updated_at
|
|
229
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
224
230
|
`).run(
|
|
225
231
|
crypto.randomUUID(),
|
|
226
232
|
donorName,
|
|
@@ -232,6 +238,7 @@ function writeDonationLog({
|
|
|
232
238
|
accessToken || '',
|
|
233
239
|
refreshToken || '',
|
|
234
240
|
expiry || '',
|
|
241
|
+
idToken || null,
|
|
235
242
|
now,
|
|
236
243
|
now
|
|
237
244
|
);
|
|
@@ -271,6 +278,9 @@ function googleRequest(method, url, data, headers = {}) {
|
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
function readFromKeychain() {
|
|
281
|
+
if (process.platform !== 'darwin') {
|
|
282
|
+
throw new Error('Fitur ambil langsung dari Keychain hanya didukung di macOS.');
|
|
283
|
+
}
|
|
274
284
|
const cmd = 'security find-generic-password -s gemini -a antigravity -w ~/Library/Keychains/login.keychain-db';
|
|
275
285
|
return execSync(cmd, { encoding: 'utf-8' }).trim();
|
|
276
286
|
}
|
|
@@ -363,16 +373,96 @@ function updateDonationLogToken(id, tokenInfo) {
|
|
|
363
373
|
}
|
|
364
374
|
}
|
|
365
375
|
|
|
376
|
+
const GOOGLE_TIERS = {
|
|
377
|
+
'free-tier': 'Free',
|
|
378
|
+
'g1-plus-tier': 'Plus',
|
|
379
|
+
'g1-pro-tier': 'Pro',
|
|
380
|
+
'g1-ultra-tier': 'Ultra'
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
function resolveGoogleTier(paidTier) {
|
|
384
|
+
const raw = paidTier || null;
|
|
385
|
+
if (!raw) return { id: 'free-tier', name: GOOGLE_TIERS['free-tier'] };
|
|
386
|
+
if (typeof raw === 'string') {
|
|
387
|
+
const id = raw.trim().toLowerCase();
|
|
388
|
+
return { id, name: GOOGLE_TIERS[id] || (id.includes('ultra') ? 'Ultra' : id.includes('pro') ? 'Pro' : id.includes('plus') ? 'Plus' : id.includes('free') ? 'Free' : raw) };
|
|
389
|
+
}
|
|
390
|
+
const id = String(raw.id || raw.name || 'free-tier').trim().toLowerCase();
|
|
391
|
+
const text = `${id} ${String(raw.name || '')}`.toLowerCase();
|
|
392
|
+
let name = GOOGLE_TIERS[id];
|
|
393
|
+
if (!name) {
|
|
394
|
+
if (text.includes('ultra')) name = 'Ultra';
|
|
395
|
+
else if (text.includes('pro')) name = 'Pro';
|
|
396
|
+
else if (text.includes('plus')) name = 'Plus';
|
|
397
|
+
else if (text.includes('free')) name = 'Free';
|
|
398
|
+
else name = raw.name || id;
|
|
399
|
+
}
|
|
400
|
+
return { id, name };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function parseTier(tierRaw, eligibilityDetailsRaw) {
|
|
404
|
+
if (tierRaw) {
|
|
405
|
+
try {
|
|
406
|
+
const parsed = JSON.parse(tierRaw);
|
|
407
|
+
if (parsed && typeof parsed === 'object') {
|
|
408
|
+
const id = String(parsed.id || '').trim().toLowerCase();
|
|
409
|
+
const text = `${id} ${String(parsed.name || '')}`.toLowerCase();
|
|
410
|
+
let name = GOOGLE_TIERS[id];
|
|
411
|
+
if (!name) {
|
|
412
|
+
if (text.includes('ultra')) name = 'Ultra';
|
|
413
|
+
else if (text.includes('pro')) name = 'Pro';
|
|
414
|
+
else if (text.includes('plus')) name = 'Plus';
|
|
415
|
+
else if (text.includes('free')) name = 'Free';
|
|
416
|
+
else name = parsed.name || id;
|
|
417
|
+
}
|
|
418
|
+
return { id: id || 'custom', name };
|
|
419
|
+
}
|
|
420
|
+
return resolveGoogleTier(tierRaw);
|
|
421
|
+
} catch {
|
|
422
|
+
return resolveGoogleTier(tierRaw);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (eligibilityDetailsRaw) {
|
|
426
|
+
try {
|
|
427
|
+
const details = typeof eligibilityDetailsRaw === 'string' ? JSON.parse(eligibilityDetailsRaw) : eligibilityDetailsRaw;
|
|
428
|
+
const pt = details?.paidTier;
|
|
429
|
+
if (pt) return resolveGoogleTier(pt);
|
|
430
|
+
} catch {}
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
366
435
|
// Update status donasi berdasarkan email (key dedupe), dipakai alur 2-fase:
|
|
367
436
|
// simpan dulu "No Verif" → setelah cek eligibility jadi Success / No Verif / Not Eligible.
|
|
368
|
-
function setDonationStatusByEmail(email, status, message) {
|
|
437
|
+
function setDonationStatusByEmail(email, status, message, eligibility) {
|
|
369
438
|
const now = new Date().toISOString();
|
|
370
439
|
const normalized = String(email || '').trim().toLowerCase();
|
|
371
440
|
if (!normalized) return;
|
|
372
441
|
const db = getLogDb();
|
|
373
442
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
443
|
+
const tier = eligibility?.paidTier || eligibility?.tier || null;
|
|
444
|
+
const tierJson = tier ? JSON.stringify(tier) : null;
|
|
445
|
+
db.prepare(`
|
|
446
|
+
UPDATE donation_logs SET
|
|
447
|
+
status = ?, message = ?,
|
|
448
|
+
eligibility_status = COALESCE(?, eligibility_status),
|
|
449
|
+
eligibility_message = COALESCE(?, eligibility_message),
|
|
450
|
+
eligibility_details = COALESCE(?, eligibility_details),
|
|
451
|
+
eligibility_checked_at = COALESCE(?, eligibility_checked_at),
|
|
452
|
+
tier = COALESCE(?, tier),
|
|
453
|
+
updated_at = ?
|
|
454
|
+
WHERE lower(donor_email) = ?
|
|
455
|
+
`).run(
|
|
456
|
+
status,
|
|
457
|
+
message || '',
|
|
458
|
+
eligibility?.status || null,
|
|
459
|
+
eligibility?.message || null,
|
|
460
|
+
eligibility?.raw ? JSON.stringify(eligibility.raw) : null,
|
|
461
|
+
eligibility ? now : null,
|
|
462
|
+
tierJson,
|
|
463
|
+
now,
|
|
464
|
+
normalized
|
|
465
|
+
);
|
|
376
466
|
} finally {
|
|
377
467
|
db.close();
|
|
378
468
|
}
|
|
@@ -383,10 +473,14 @@ function setDonationStatusById(id, status, message, eligibility) {
|
|
|
383
473
|
if (!id) return;
|
|
384
474
|
const db = getLogDb();
|
|
385
475
|
try {
|
|
476
|
+
const tier = eligibility?.paidTier || eligibility?.tier || null;
|
|
477
|
+
const tierJson = tier ? JSON.stringify(tier) : null;
|
|
386
478
|
db.prepare(`
|
|
387
479
|
UPDATE donation_logs SET
|
|
388
480
|
status = ?, message = ?, eligibility_status = ?, eligibility_message = ?,
|
|
389
|
-
eligibility_details = ?, eligibility_checked_at = ?,
|
|
481
|
+
eligibility_details = ?, eligibility_checked_at = ?,
|
|
482
|
+
tier = COALESCE(?, tier),
|
|
483
|
+
updated_at = ?
|
|
390
484
|
WHERE id = ?
|
|
391
485
|
`).run(
|
|
392
486
|
status,
|
|
@@ -395,6 +489,7 @@ function setDonationStatusById(id, status, message, eligibility) {
|
|
|
395
489
|
eligibility?.message || '',
|
|
396
490
|
JSON.stringify(eligibility?.raw || {}),
|
|
397
491
|
now,
|
|
492
|
+
tierJson,
|
|
398
493
|
now,
|
|
399
494
|
id
|
|
400
495
|
);
|
|
@@ -426,16 +521,17 @@ async function checkAntigravityEligibility(accessToken) {
|
|
|
426
521
|
'Content-Type': 'application/json',
|
|
427
522
|
'User-Agent': userAgent
|
|
428
523
|
});
|
|
429
|
-
const
|
|
524
|
+
const paidTier = resolveGoogleTier(response.paidTier);
|
|
525
|
+
const tier = paidTier;
|
|
430
526
|
const ineligibleTiers = Array.isArray(response.ineligibleTiers) ? response.ineligibleTiers : [];
|
|
431
527
|
const ineligible = ineligibleTiers[0] || null;
|
|
432
528
|
const isEligible = ineligibleTiers.length === 0;
|
|
433
529
|
const needsVerification = ineligible?.reasonCode === 'VALIDATION_REQUIRED';
|
|
434
530
|
const result = isEligible
|
|
435
|
-
? { eligible: true, status: 'eligible', needVerification: false,
|
|
531
|
+
? { eligible: true, status: 'eligible', needVerification: false, paidTier, tier, validationUrl: null, message: 'Terimakasih! partisipasi dukungan token Antigravity dari kamu sangat membantu.' }
|
|
436
532
|
: needsVerification
|
|
437
|
-
? { eligible: false, status: 'verification_required', needVerification: true,
|
|
438
|
-
: { eligible: false, status: 'failed', needVerification: false,
|
|
533
|
+
? { eligible: false, status: 'verification_required', needVerification: true, paidTier, tier, validationUrl: ineligible.validationUrl || null, message: ineligible.validationErrorMessage || ineligible.reasonMessage || 'Akun Google Anda perlu diverifikasi.' }
|
|
534
|
+
: { eligible: false, status: 'failed', needVerification: false, paidTier, tier, validationUrl: null, message: ineligible.reasonMessage || response.error?.message || 'Akun tidak eligible untuk Antigravity.' };
|
|
439
535
|
// console.log('[eligibility]', JSON.stringify({ endpoint, httpStatus: response._httpStatus || null, status: result.status, reasonCode: ineligible?.reasonCode || null, validationUrlAvailable: Boolean(result.validationUrl) }));
|
|
440
536
|
return { ...result, raw: response };
|
|
441
537
|
}
|
|
@@ -811,14 +907,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
811
907
|
message: 'Menunggu pengecekan eligibility',
|
|
812
908
|
accessToken: tokenResponse.access_token,
|
|
813
909
|
refreshToken: tokenResponse.refresh_token,
|
|
814
|
-
expiry
|
|
910
|
+
expiry,
|
|
911
|
+
idToken: tokenResponse.id_token || null
|
|
815
912
|
});
|
|
816
913
|
|
|
817
914
|
// Fase 2: cek eligibility, baru finalkan statusnya.
|
|
818
915
|
const eligibility = await checkAntigravityEligibility(tokenResponse.access_token);
|
|
819
916
|
if (eligibility.status !== 'eligible') {
|
|
820
917
|
const logStatus = eligibility.status === 'verification_required' ? 'No Verif' : 'Not Eligible';
|
|
821
|
-
setDonationStatusByEmail(userResponse.email, logStatus, eligibility.message);
|
|
918
|
+
setDonationStatusByEmail(userResponse.email, logStatus, eligibility.message, eligibility);
|
|
822
919
|
const checkId = eligibility.status === 'verification_required' ? rememberPendingEligibility({
|
|
823
920
|
name: userResponse.name || userResponse.email,
|
|
824
921
|
email: userResponse.email,
|
|
@@ -835,7 +932,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
835
932
|
eligible: false,
|
|
836
933
|
status: eligibility.status,
|
|
837
934
|
needVerification: eligibility.needVerification,
|
|
838
|
-
|
|
935
|
+
paidTier: eligibility.paidTier,
|
|
839
936
|
tier: eligibility.tier,
|
|
840
937
|
validationUrl: eligibility.validationUrl,
|
|
841
938
|
verificationUrl: eligibility.validationUrl,
|
|
@@ -846,11 +943,12 @@ const server = http.createServer(async (req, res) => {
|
|
|
846
943
|
});
|
|
847
944
|
}
|
|
848
945
|
|
|
849
|
-
setDonationStatusByEmail(userResponse.email, 'Success', message);
|
|
946
|
+
setDonationStatusByEmail(userResponse.email, 'Success', message, eligibility);
|
|
850
947
|
return sendJSON(res, 200, {
|
|
851
948
|
success: true,
|
|
852
949
|
status: eligibility.status,
|
|
853
|
-
|
|
950
|
+
paidTier: eligibility.paidTier,
|
|
951
|
+
tier: eligibility.tier,
|
|
854
952
|
token: {
|
|
855
953
|
access_token: tokenResponse.access_token,
|
|
856
954
|
token_type: tokenResponse.token_type || 'Bearer',
|
|
@@ -907,11 +1005,11 @@ const server = http.createServer(async (req, res) => {
|
|
|
907
1005
|
const eligibility = await checkAntigravityEligibility(token.accessToken);
|
|
908
1006
|
if (eligibility.status !== 'eligible') {
|
|
909
1007
|
const logStatus = eligibility.status === 'verification_required' ? 'No Verif' : 'Not Eligible';
|
|
910
|
-
setDonationStatusByEmail(email, logStatus, eligibility.message);
|
|
1008
|
+
setDonationStatusByEmail(email, logStatus, eligibility.message, eligibility);
|
|
911
1009
|
return sendJSON(res, 403, { success: false, ...eligibility, details: eligibility.raw, email, checkId: body.checkId });
|
|
912
1010
|
}
|
|
913
1011
|
if (pending) pendingEligibilityChecks.delete(String(body.checkId));
|
|
914
|
-
setDonationStatusByEmail(email, 'Success', eligibility.message);
|
|
1012
|
+
setDonationStatusByEmail(email, 'Success', eligibility.message, eligibility);
|
|
915
1013
|
return sendJSON(res, 200, { success: true, message: 'Terimakasih! partisipasi dukungan token Antigravity dari kamu sangat membantu.', ...eligibility });
|
|
916
1014
|
} catch (error) {
|
|
917
1015
|
return sendJSON(res, 500, { success: false, error: 'Eligibility recheck gagal: ' + error.message });
|
|
@@ -985,7 +1083,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
985
1083
|
eligible: result.eligible,
|
|
986
1084
|
status: result.status,
|
|
987
1085
|
needVerification: result.needVerification,
|
|
988
|
-
|
|
1086
|
+
paidTier: result.paidTier,
|
|
989
1087
|
tier: result.tier,
|
|
990
1088
|
validationUrl: result.validationUrl,
|
|
991
1089
|
verificationUrl: result.validationUrl,
|
|
@@ -1001,7 +1099,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
1001
1099
|
try {
|
|
1002
1100
|
const raw = readFromKeychain();
|
|
1003
1101
|
const decoded = decodeKeychainData(raw);
|
|
1004
|
-
return sendJSON(res, 200, {
|
|
1102
|
+
return sendJSON(res, 200, {
|
|
1103
|
+
success: true,
|
|
1104
|
+
token: {
|
|
1105
|
+
accessToken: decoded.token.access_token,
|
|
1106
|
+
refreshToken: decoded.token.refresh_token,
|
|
1107
|
+
expiry: decoded.token.expiry,
|
|
1108
|
+
idToken: decoded.id_token || null
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1005
1111
|
} catch (error) { return sendJSON(res, 500, { success: false, error: error.message }); }
|
|
1006
1112
|
}
|
|
1007
1113
|
|
|
@@ -1010,7 +1116,15 @@ const server = http.createServer(async (req, res) => {
|
|
|
1010
1116
|
try {
|
|
1011
1117
|
const body = await parseBody(req);
|
|
1012
1118
|
const decoded = decodeKeychainData(body.raw.trim());
|
|
1013
|
-
return sendJSON(res, 200, {
|
|
1119
|
+
return sendJSON(res, 200, {
|
|
1120
|
+
success: true,
|
|
1121
|
+
token: {
|
|
1122
|
+
accessToken: decoded.token.access_token,
|
|
1123
|
+
refreshToken: decoded.token.refresh_token,
|
|
1124
|
+
expiry: decoded.token.expiry,
|
|
1125
|
+
idToken: decoded.id_token || null
|
|
1126
|
+
}
|
|
1127
|
+
});
|
|
1014
1128
|
} catch (error) { return sendJSON(res, 500, { success: false, error: 'Gagal decode: ' + error.message }); }
|
|
1015
1129
|
}
|
|
1016
1130
|
|
|
@@ -1198,6 +1312,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1198
1312
|
status: result.status,
|
|
1199
1313
|
logStatus,
|
|
1200
1314
|
message: result.message,
|
|
1315
|
+
paidTier: result.paidTier,
|
|
1201
1316
|
tier: result.tier,
|
|
1202
1317
|
validationUrl: result.validationUrl,
|
|
1203
1318
|
verificationUrl: result.validationUrl,
|
|
@@ -1495,7 +1610,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1495
1610
|
: 'Not Eligible';
|
|
1496
1611
|
setDonationStatusById(row.id, logStatus, result.message, result);
|
|
1497
1612
|
recap[logStatus] = (recap[logStatus] || 0) + 1;
|
|
1498
|
-
send({ type: 'log', index: index + 1, email: row.donor_email, action: logStatus, status: result.status, message: result.message, tokenRefreshed: token.refreshed });
|
|
1613
|
+
send({ type: 'log', index: index + 1, email: row.donor_email, action: logStatus, status: result.status, tier: result.tier, message: result.message, tokenRefreshed: token.refreshed });
|
|
1499
1614
|
} catch (error) {
|
|
1500
1615
|
errors++;
|
|
1501
1616
|
const message = error.message || 'Eligibility check gagal';
|
|
@@ -1522,24 +1637,29 @@ const server = http.createServer(async (req, res) => {
|
|
|
1522
1637
|
const db = getLogDb();
|
|
1523
1638
|
const rows = db.prepare(`
|
|
1524
1639
|
SELECT donor_name, donor_email, requested_for, source, status,
|
|
1525
|
-
created_at, access_token, refresh_token, expiry
|
|
1640
|
+
created_at, access_token, refresh_token, expiry, tier, eligibility_details
|
|
1526
1641
|
FROM donation_logs
|
|
1527
1642
|
ORDER BY created_at DESC
|
|
1528
1643
|
`).all();
|
|
1529
1644
|
db.close();
|
|
1530
1645
|
|
|
1531
|
-
const header = ['Name', 'Email', 'RequestedFor', 'Source', 'Status', 'CreatedAt', 'AccessToken', 'RefreshToken', 'Expiry'];
|
|
1532
|
-
const csv = [header, ...rows.map((row) =>
|
|
1533
|
-
row.
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1646
|
+
const header = ['Name', 'Email', 'RequestedFor', 'Source', 'Status', 'Tier', 'CreatedAt', 'AccessToken', 'RefreshToken', 'Expiry'];
|
|
1647
|
+
const csv = [header, ...rows.map((row) => {
|
|
1648
|
+
const tierObj = parseTier(row.tier, row.eligibility_details);
|
|
1649
|
+
const tierName = tierObj ? tierObj.name : '-';
|
|
1650
|
+
return [
|
|
1651
|
+
row.donor_name,
|
|
1652
|
+
row.donor_email,
|
|
1653
|
+
row.requested_for,
|
|
1654
|
+
row.source,
|
|
1655
|
+
row.status,
|
|
1656
|
+
tierName,
|
|
1657
|
+
row.created_at,
|
|
1658
|
+
row.access_token || '',
|
|
1659
|
+
row.refresh_token || '',
|
|
1660
|
+
row.expiry || '',
|
|
1661
|
+
];
|
|
1662
|
+
})]
|
|
1543
1663
|
.map((row) => row.map((col) => `"${String(col).replace(/"/g, '""')}"`).join(','))
|
|
1544
1664
|
.join('\r\n');
|
|
1545
1665
|
|
|
@@ -1561,6 +1681,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
1561
1681
|
const search = String(parsedUrl.searchParams.get('search') || '').trim().toLowerCase();
|
|
1562
1682
|
const statusFilter = String(parsedUrl.searchParams.get('status') || '').trim();
|
|
1563
1683
|
const sourceFilter = String(parsedUrl.searchParams.get('source') || '').trim();
|
|
1684
|
+
const tierFilter = String(parsedUrl.searchParams.get('tier') || '').trim().toLowerCase();
|
|
1564
1685
|
const where = [];
|
|
1565
1686
|
const params = [];
|
|
1566
1687
|
if (search) {
|
|
@@ -1575,6 +1696,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
1575
1696
|
where.push('source = ?');
|
|
1576
1697
|
params.push(sourceFilter);
|
|
1577
1698
|
}
|
|
1699
|
+
if (tierFilter) {
|
|
1700
|
+
if (tierFilter === 'ultra') {
|
|
1701
|
+
where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%ultra%')");
|
|
1702
|
+
} else if (tierFilter === 'pro') {
|
|
1703
|
+
where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%pro%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%ultra%')");
|
|
1704
|
+
} else if (tierFilter === 'plus') {
|
|
1705
|
+
where.push("(lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%plus%')");
|
|
1706
|
+
} else if (tierFilter === 'free') {
|
|
1707
|
+
where.push("((lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) LIKE '%free%') OR ((tier IS NOT NULL OR eligibility_details IS NOT NULL) AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%plus%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%pro%' AND lower(coalesce(tier, '') || ' ' || coalesce(eligibility_details, '')) NOT LIKE '%ultra%'))");
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1578
1710
|
const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
|
|
1579
1711
|
const db = getLogDb();
|
|
1580
1712
|
const total = db.prepare(`SELECT COUNT(*) AS count FROM donation_logs ${whereSql}`).get(...params).count;
|
|
@@ -1599,19 +1731,25 @@ const server = http.createServer(async (req, res) => {
|
|
|
1599
1731
|
access_token AS accessToken,
|
|
1600
1732
|
refresh_token AS refreshToken,
|
|
1601
1733
|
expiry,
|
|
1734
|
+
id_token AS idToken,
|
|
1602
1735
|
eligibility_status AS eligibilityStatus,
|
|
1603
1736
|
eligibility_message AS eligibilityMessage,
|
|
1604
1737
|
eligibility_details AS eligibilityDetails,
|
|
1605
1738
|
eligibility_checked_at AS eligibilityCheckedAt,
|
|
1739
|
+
tier,
|
|
1606
1740
|
created_at AS createdAt
|
|
1607
1741
|
FROM donation_logs
|
|
1608
1742
|
${whereSql}
|
|
1609
1743
|
ORDER BY created_at DESC
|
|
1610
1744
|
LIMIT ? OFFSET ?
|
|
1611
|
-
`).all(...params, pageSize, (page - 1) * pageSize)
|
|
1745
|
+
`).all(...params, pageSize, (page - 1) * pageSize).map((row) => ({
|
|
1746
|
+
...row,
|
|
1747
|
+
tier: parseTier(row.tier, row.eligibilityDetails)
|
|
1748
|
+
}));
|
|
1612
1749
|
const filterOptions = {
|
|
1613
1750
|
statuses: db.prepare("SELECT DISTINCT status AS value FROM donation_logs WHERE status IS NOT NULL AND TRIM(status) != '' ORDER BY status ASC").all().map((row) => row.value),
|
|
1614
1751
|
sources: db.prepare("SELECT DISTINCT source AS value FROM donation_logs WHERE source IS NOT NULL AND TRIM(source) != '' ORDER BY source ASC").all().map((row) => row.value),
|
|
1752
|
+
tiers: ['Free', 'Plus', 'Pro', 'Ultra'],
|
|
1615
1753
|
};
|
|
1616
1754
|
db.close();
|
|
1617
1755
|
return sendJSON(res, 200, { success: true, donations: rows, page, pageSize, total, pageCount, stats, filterOptions });
|