antigravity-tc 1.1.0
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/LICENSE +21 -0
- package/README.md +135 -0
- package/bin/reset-password.js +96 -0
- package/build.md +82 -0
- package/dev.md +102 -0
- package/env.example +12 -0
- package/images/Dashboard.jpg +0 -0
- package/images/Pengaturan.jpg +0 -0
- package/images/Sync.jpg +0 -0
- package/images/antigravity-tc.jpg +0 -0
- package/index.js +182 -0
- package/package.json +67 -0
- package/server.js +1366 -0
- package/src/autostart.js +115 -0
- package/src/cli.js +82 -0
- package/src/config.js +23 -0
- package/src/jwt.js +37 -0
- package/src/paths.js +59 -0
- package/src/tray.js +210 -0
- package/src/tunnel.js +198 -0
- package/web/dist/admin.html +22 -0
- package/web/dist/assets/admin-Boto8g_O.js +4 -0
- package/web/dist/assets/admin-DPInPm4J.css +1 -0
- package/web/dist/assets/index-BBB1az1b.js +1 -0
- package/web/dist/assets/index-Cb27-wjB.css +1 -0
- package/web/dist/assets/styles-WqBkvfmY.css +1 -0
- package/web/dist/assets/styles-pGt3BQwq.js +2 -0
- package/web/dist/ex.jpeg +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/index.html +22 -0
- package/web/dist/robots.txt +2 -0
- package/web/public/ex.jpeg +0 -0
- package/web/public/favicon.ico +0 -0
- package/web/public/robots.txt +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-2026 decolua and contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="images/antigravity-tc.jpg" alt="Antigravity Token Collector" width="860" />
|
|
4
|
+
|
|
5
|
+
# Antigravity Token Collector
|
|
6
|
+
|
|
7
|
+
**Kumpulkan donasi token Antigravity dari siapa saja — langsung masuk ke database [9Router](https://github.com/decolua/9router) Anda.**
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/antigravity-tc)
|
|
10
|
+
[](LICENSE)
|
|
11
|
+
[](#-dukungan-platform)
|
|
12
|
+
[]()
|
|
13
|
+
|
|
14
|
+
Tray app + dashboard untuk mengelola donasi token: halaman donasi publik, tunnel gratis tanpa expose IP, auto-start saat login, dan sync massal ke 9Router.
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ⚡ Quick Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g antigravity-tc
|
|
24
|
+
antigravity-tc
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
🎉 Dashboard terbuka di `http://localhost:3456/dashboard` — tray icon muncul di menubar.
|
|
28
|
+
|
|
29
|
+
__That's it!__ 🚀
|
|
30
|
+
|
|
31
|
+
> 💡 Password default: `1234567890` — hint tampil di halaman login selama belum diganti. Segera ganti di **Pengaturan → Keamanan**.
|
|
32
|
+
>
|
|
33
|
+
> 🔑 **Lupa password?** Jalankan `antigravity-tc-reset-password` di terminal untuk meresetnya kembali ke default.
|
|
34
|
+
|
|
35
|
+
## 🔄 Cara Kerja
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
┌──────────────────┐ ┌─────────────────────┐ ┌──────────────────┐
|
|
39
|
+
│ Donatur │ │ ATC (Anda) │ │ 9Router │
|
|
40
|
+
│ │ │ │ │ │
|
|
41
|
+
│ Login Google ───┼──────▶ │ Cek eligibility ───┼──────▶ │ Token tersimpan │
|
|
42
|
+
│ lewat link yang │ │ Refresh otomatis │ │ siap dipakai │
|
|
43
|
+
│ Anda bagikan │ │ Anti-duplikat │ │ │
|
|
44
|
+
└──────────────────┘ └─────────────────────┘ └──────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
1. Jalankan `antigravity-tc` — dashboard & tray aktif.
|
|
48
|
+
2. Buat **link donasi personal** dari dashboard (Generate Link), bagikan lewat chat mana pun — atau aktifkan **tunnel publik** gratis supaya bisa diakses dari luar tanpa expose IP.
|
|
49
|
+
3. Donatur login Google + tempel kode — token otomatis dicek, di-refresh bila expired, lalu tersimpan ke 9Router.
|
|
50
|
+
|
|
51
|
+
## 🖼️ Tampilan
|
|
52
|
+
|
|
53
|
+
| Dashboard | Sync Massal | Pengaturan |
|
|
54
|
+
| :---: | :---: | :---: |
|
|
55
|
+
|  |  |  |
|
|
56
|
+
|
|
57
|
+
## 📋 Fitur
|
|
58
|
+
|
|
59
|
+
| Fitur | Keterangan |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| 🌐 Halaman donasi publik | Login Google + tempel kode, token dicek eligibility lalu disimpan |
|
|
62
|
+
| 🔗 Generate link personal | `?for=Nama` — sapaan otomatis untuk tiap calon donatur |
|
|
63
|
+
| 📊 Dashboard admin | Statistik, tabel live, export & import CSV, generate link |
|
|
64
|
+
| 🔄 Sync massal ke 9Router | Upsert semua data sekaligus dengan log terminal realtime |
|
|
65
|
+
| ⚡ Auto refresh token | Token expired otomatis di-refresh Google saat check eligibility |
|
|
66
|
+
| 🌐 Tunnel publik gratis | Cloudflare Quick Tunnel — URL `*.trycloudflare.com` siap dibagikan |
|
|
67
|
+
| 🚀 Auto start | Berjalan otomatis (mode tray) saat OS login |
|
|
68
|
+
| 🔒 Keamanan | Login JWT (secret unik per instalasi), tunnel terkunci selama password default |
|
|
69
|
+
|
|
70
|
+
## 📁 Data Location
|
|
71
|
+
|
|
72
|
+
Lokasi default **mengikuti OS tempat package di-install** — tanpa konfigurasi tambahan:
|
|
73
|
+
|
|
74
|
+
### Database 9Router (target penyimpanan token)
|
|
75
|
+
|
|
76
|
+
| OS | Lokasi Default |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| macOS / Linux | `~/.9router/db/data.sqlite` |
|
|
79
|
+
| Windows | `%APPDATA%/9router/db/data.sqlite` |
|
|
80
|
+
| Docker | `/app/data/db/data.sqlite` (mount volume untuk persist) |
|
|
81
|
+
|
|
82
|
+
> Path ini bisa dioverride kapan saja via **Dashboard → Pengaturan → Database 9Router** (untuk instalasi non-standar) atau env `DB_PATH`.
|
|
83
|
+
|
|
84
|
+
### Data ATC (log donasi, binary tunnel, config)
|
|
85
|
+
|
|
86
|
+
| OS | Lokasi |
|
|
87
|
+
| --- | --- |
|
|
88
|
+
| macOS / Linux | `~/.antigravity-tc/` |
|
|
89
|
+
| Windows | `%USERPROFILE%\.antigravity-tc\` |
|
|
90
|
+
| Docker | `/root/.antigravity-tc/` (mount `$HOME/.antigravity-tc` untuk persist) |
|
|
91
|
+
|
|
92
|
+
| Isi | Keterangan |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `logs/donation-logs.sqlite` | Log donasi (auto-migrasi dari instalasi lama) |
|
|
95
|
+
| `bin/cloudflared` | Binary tunnel (auto-download per OS) |
|
|
96
|
+
| `logs/launcher.log` | Log saat auto-start |
|
|
97
|
+
|
|
98
|
+
## 🖥️ Dukungan Platform
|
|
99
|
+
|
|
100
|
+
| Kemampuan | macOS | Linux | Windows | Docker |
|
|
101
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
102
|
+
| Server + Dashboard + halaman donasi | ✅ | ✅ | ✅ | ✅ |
|
|
103
|
+
| Tray icon | ✅ | ✅ | ✅ | — |
|
|
104
|
+
| Auto-start saat login | ✅ | ✅ | ✅ | — (pakai restart policy) |
|
|
105
|
+
| Cloudflare Tunnel | ✅ | ✅ | ✅ | ✅ |
|
|
106
|
+
| Path DB 9Router otomatis per OS | ✅ | ✅ | ✅ | ✅ |
|
|
107
|
+
|
|
108
|
+
> Docker: jalankan dengan `antigravity-tc --hidden` sebagai entrypoint dan mount volume data (lihat tabel di atas). Deteksi Docker otomatis memakai path `/app/data/db/data.sqlite`.
|
|
109
|
+
|
|
110
|
+
## 🔑 Password & `.env`
|
|
111
|
+
|
|
112
|
+
- Password: default `1234567890` → ganti via dashboard, atau set `APP_PASSWORD` di `.env`.
|
|
113
|
+
- Prioritas: password dari dashboard > `APP_PASSWORD` > default.
|
|
114
|
+
- Kredensial OAuth **sudah tertanam bawaan** (memakai kredensial publik Antigravity CLI) — `.env` hanya perlu diisi `PORT`/`APP_PASSWORD` bila mau. Override `OAUTH_CLIENT_ID`/`OAUTH_CLIENT_SECRET` tersedia untuk OAuth client sendiri.
|
|
115
|
+
|
|
116
|
+
## ❓ Troubleshooting
|
|
117
|
+
|
|
118
|
+
| Masalah | Solusi |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| Lupa password admin | Jalankan `antigravity-tc-reset-password` (tambah `--yes` untuk tanpa konfirmasi) |
|
|
121
|
+
| Tunnel tidak bisa diaktifkan | Ganti password default dulu di Pengaturan → Keamanan |
|
|
122
|
+
| Tray icon tidak muncul | Pastikan binary `systray2/traybin/` executable |
|
|
123
|
+
| Sudah berjalan tapi tidak bisa dibuka lagi | App single-instance — cukup klik tray icon |
|
|
124
|
+
| Refresh token gagal | Cek koneksi; kredensial OAuth sudah bawaan — atau isi `OAUTH_CLIENT_ID` & `OAUTH_CLIENT_SECRET` di `.env` untuk client sendiri |
|
|
125
|
+
| DB 9Router tidak ketemu | Cek path di Pengaturan → Database 9Router |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 🛠️ Development
|
|
130
|
+
|
|
131
|
+
Untuk menjalankan dari source, development, dan build/publish mandiri — lihat **[dev.md](dev.md)** dan **[build.md](build.md)**.
|
|
132
|
+
|
|
133
|
+
## 📄 License
|
|
134
|
+
|
|
135
|
+
[MIT](LICENSE) © decolua and contributors
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Hard reset password admin ATC kembali ke default.
|
|
3
|
+
// Pakai: antigravity-tc-reset-password [--yes]
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
|
|
9
|
+
// .env selalu dibaca dari folder aplikasi (konsisten dengan index.js).
|
|
10
|
+
require('dotenv').config({ path: path.join(__dirname, '..', '.env'), quiet: true });
|
|
11
|
+
|
|
12
|
+
const { getConfig } = require('../src/config');
|
|
13
|
+
|
|
14
|
+
const DEFAULT_APP_PASSWORD = '1234567890';
|
|
15
|
+
|
|
16
|
+
function effectivePassword(config) {
|
|
17
|
+
return config.get('adminPassword') || process.env.APP_PASSWORD || DEFAULT_APP_PASSWORD;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isAppRunning(port) {
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const request = http.get(`http://127.0.0.1:${port}/api/password-status`, (res) => {
|
|
23
|
+
res.resume();
|
|
24
|
+
resolve(res.statusCode === 200);
|
|
25
|
+
});
|
|
26
|
+
request.on('error', () => resolve(false));
|
|
27
|
+
request.setTimeout(1000, () => {
|
|
28
|
+
request.destroy();
|
|
29
|
+
resolve(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ask(question) {
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
rl.question(question, (answer) => {
|
|
38
|
+
rl.close();
|
|
39
|
+
resolve(answer.trim().toLowerCase());
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function main() {
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
const configuredPort = Number(process.env.PORT || config.get('port') || 3456);
|
|
47
|
+
const customPassword = config.get('adminPassword');
|
|
48
|
+
|
|
49
|
+
console.log('========================================');
|
|
50
|
+
console.log(' Antigravity TC — Reset Password Admin');
|
|
51
|
+
console.log('========================================');
|
|
52
|
+
console.log('');
|
|
53
|
+
|
|
54
|
+
if (!customPassword && process.env.APP_PASSWORD) {
|
|
55
|
+
console.log('Password saat ini berasal dari APP_PASSWORD di file .env,');
|
|
56
|
+
console.log('bukan dari dashboard — reset di sini tidak akan mengubahnya.');
|
|
57
|
+
console.log('');
|
|
58
|
+
console.log('Solusi : hapus/ubah baris APP_PASSWORD di .env, lalu restart aplikasi.');
|
|
59
|
+
console.log(`Lokasi : ${path.join(__dirname, '..', '.env')}`);
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!customPassword) {
|
|
64
|
+
console.log(`Password sudah default (${DEFAULT_APP_PASSWORD}). Tidak ada yang perlu direset.`);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const running = await isAppRunning(configuredPort);
|
|
69
|
+
if (running) {
|
|
70
|
+
console.log(`⚠️ Aplikasi sedang berjalan di port ${configuredPort}.`);
|
|
71
|
+
console.log(' Setelah reset, restart aplikasi (Exit dari tray lalu jalankan lagi)');
|
|
72
|
+
console.log(' supaya password baru terbaca.');
|
|
73
|
+
console.log('');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const skipConfirm = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
77
|
+
if (!skipConfirm) {
|
|
78
|
+
const answer = await ask(`Reset password kembali ke default "${DEFAULT_APP_PASSWORD}"? (y/N): `);
|
|
79
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
80
|
+
console.log('Dibatalkan. Password tidak diubah.');
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
config.set('adminPassword', null);
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log('✅ Password admin direset.');
|
|
88
|
+
console.log(` Password aktif sekarang: ${effectivePassword(config)}`);
|
|
89
|
+
if (running) console.log(' Jangan lupa restart aplikasinya.');
|
|
90
|
+
console.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
main().catch((error) => {
|
|
94
|
+
console.error('Gagal reset password:', error.message);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
package/build.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# 📦 Build & Publish Guide
|
|
2
|
+
|
|
3
|
+
Panduan build frontend, verifikasi paket, dan publish ke npmjs.com.
|
|
4
|
+
|
|
5
|
+
## Build Frontend
|
|
6
|
+
|
|
7
|
+
Frontend (Vue 3 + Vite) harus di-build ke `web/dist/` — server Node melayaninya sebagai file statis.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run build:web
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Hasil: `web/dist/` berisi `index.html` (halaman publik), `admin.html` (dashboard), `assets/*` (JS/CSS hashed), plus favicon/gambar dari `web/public/`.
|
|
14
|
+
|
|
15
|
+
> Tanpa `web/dist/`, halaman `/` dan `/dashboard` akan 404 dengan pesan "Jalankan npm run build:web".
|
|
16
|
+
|
|
17
|
+
### Struktur Output
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
web/dist/
|
|
21
|
+
├── index.html ← halaman donasi publik (/)
|
|
22
|
+
├── admin.html ← dashboard (/dashboard)
|
|
23
|
+
├── favicon.ico, ex.jpeg, robots.txt
|
|
24
|
+
└── assets/ ← JS/CSS dengan hash (cache immutable)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Sebelum Publish (Checklist)
|
|
28
|
+
|
|
29
|
+
1. **Isi `author`** di `package.json` (nama/npm username Anda).
|
|
30
|
+
2. **Naikkan versi**:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm version patch # 1.0.0 → 1.0.1 (bugfix)
|
|
34
|
+
npm version minor # 1.0.0 → 1.1.0 (fitur baru)
|
|
35
|
+
npm version major # 1.0.0 → 2.0.0 (breaking change)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. **Lint bersih**: `npm run lint`
|
|
39
|
+
4. **Verifikasi isi paket** (yang penting — pastikan secret tidak ikut):
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm pack --dry-run
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Yang boleh ikut: `index.js`, `server.js`, `src/`, `web/dist/`, `web/public/`, `env.example`, `README.md`, `package.json`.
|
|
46
|
+
Yang TIDAK boleh ikut: `.env`, `*.sqlite`, `web/src/` (source frontend — pengguna cukup pakai dist).
|
|
47
|
+
|
|
48
|
+
Whitelist dikontrol field `files` di `package.json`.
|
|
49
|
+
|
|
50
|
+
## Publish
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm login # sekali saja
|
|
54
|
+
npm publish
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Script `prepublishOnly` otomatis menjalankan **lint + build:web** sebelum paket dibuat — build tidak akan basi.
|
|
58
|
+
|
|
59
|
+
Setelah publish, pengguna cukup:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm install -g antigravity-tc
|
|
63
|
+
antigravity-tc
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Setelah Publish — Verifikasi Cepat
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# di folder lain / mesin bersih
|
|
70
|
+
npm install -g antigravity-tc
|
|
71
|
+
antigravity-tc # dashboard + tray jalan
|
|
72
|
+
antigravity-tc --hidden # mode tray langsung
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Cek juga `~/.antigravity-tc/` terbuat dan halaman `/dashboard` terbuka.
|
|
76
|
+
|
|
77
|
+
## Catatan Build
|
|
78
|
+
|
|
79
|
+
- **Vite multi-page**: konfigurasi `web/vite.config.mjs` (entry `index` + `admin`).
|
|
80
|
+
- **Tailwind via PostCSS**: `web/postcss.config.js` mengarah ke `web/tailwind.config.js` secara eksplisit; content glob memakai path absolut.
|
|
81
|
+
- **`vue` & `vite` adalah devDependencies** — pengguna paket tidak mendownload dependency frontend sama sekali; mereka hanya menerima `web/dist/`.
|
|
82
|
+
- **Icon tray** dibaca dari `web/public/favicon.ico` — folder ini ikut di-publish demi itu.
|
package/dev.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 🛠️ Development Guide
|
|
2
|
+
|
|
3
|
+
Panduan untuk menjalankan dari source, memodifikasi backend/frontend, dan konvensi proyek.
|
|
4
|
+
|
|
5
|
+
## Prasyarat
|
|
6
|
+
|
|
7
|
+
- **Node.js ≥ 20** (diuji pada v24)
|
|
8
|
+
- **macOS** (tray icon, Keychain, dan LaunchAgent spesifik macOS)
|
|
9
|
+
- npm
|
|
10
|
+
|
|
11
|
+
## Setup
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone <repo-anda> antigravity-tc
|
|
15
|
+
cd antigravity-tc
|
|
16
|
+
npm install
|
|
17
|
+
cp env.example .env # lalu isi nilai
|
|
18
|
+
npm run build:web # build frontend sekali sebelum jalan dari source
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
File `.env` **selalu dibaca dari folder aplikasi** (bukan cwd) — jadi kredensial tetap terbaca walau dijalankan dari direktori lain (npm link / LaunchAgent).
|
|
22
|
+
|
|
23
|
+
### Variabel `.env`
|
|
24
|
+
|
|
25
|
+
| Variabel | Keterangan |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `PORT` | Port server (default `3456`) |
|
|
28
|
+
| `APP_PASSWORD` | Password admin (jika kosong: `1234567890`; password yang diganti via dashboard selalu menang) |
|
|
29
|
+
| `DB_PATH` | Path `data.sqlite` 9Router (opsional; bisa juga dari dashboard) |
|
|
30
|
+
| `OAUTH_CLIENT_ID` / `OAUTH_CLIENT_SECRET` | Opsional — kredensial OAuth publik Antigravity CLI sudah tertanam bawaan di `server.js` (base64); isi hanya untuk memakai client sendiri |
|
|
31
|
+
| `OAUTH_METHOD` | `tab` (default) atau `popup` |
|
|
32
|
+
|
|
33
|
+
## Menjalankan
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm start # TUI interaktif + tray
|
|
37
|
+
node index.js --hidden # langsung mode tray
|
|
38
|
+
npm run dev:web # frontend: Vite dev server di :5173 (proxy API ke :3456)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Alur development frontend: jalankan `npm start` (backend) + `npm run dev:web` (frontend) di terminal berbeda, lalu buka `http://localhost:5173` — hot reload setiap edit `web/src/**`.
|
|
42
|
+
|
|
43
|
+
## Struktur Proyek
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
index.js Entry point: single-instance lock, tray, TUI, lifecycle
|
|
47
|
+
server.js HTTP server + semua API route
|
|
48
|
+
src/
|
|
49
|
+
paths.js Path ~/.antigravity-tc/ + migrasi database
|
|
50
|
+
config.js Config (library `conf`)
|
|
51
|
+
jwt.js Auth JWT — secret random per instalasi
|
|
52
|
+
tunnel.js Cloudflare Quick Tunnel (download binary, spawn, parse URL)
|
|
53
|
+
autostart.js LaunchAgent macOS (di tulis native, bukan via auto-launch)
|
|
54
|
+
tray.js Tray icon macOS (systray2)
|
|
55
|
+
cli.js TUI menu (@inquirer/prompts)
|
|
56
|
+
web/ Frontend Vue 3 + Vite + Tailwind (lihat build.md)
|
|
57
|
+
src/admin/ Dashboard admin (App, LoginCard, DashboardView, SettingsView, store.js)
|
|
58
|
+
src/public/ Halaman donasi publik (PKCE, modal eligibility)
|
|
59
|
+
src/shared/ Komponen & helper bersama (Mascot, api, toast)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API Endpoints
|
|
63
|
+
|
|
64
|
+
Semua endpoint `/api/*` selain `verify-password`, `session`, `logout`, `password-status`, dan `oauth-config` membutuhkan **JWT admin** (cookie `admin_session` httpOnly).
|
|
65
|
+
|
|
66
|
+
| Endpoint | Method | Fungsi |
|
|
67
|
+
| --- | --- | --- |
|
|
68
|
+
| `/api/verify-password` | POST | Login → set cookie JWT |
|
|
69
|
+
| `/api/password` | POST | Ganti password (admin) |
|
|
70
|
+
| `/api/settings` | GET/POST | Baca/ubah pengaturan (path DB 9Router) |
|
|
71
|
+
| `/api/autostart` | POST | Toggle auto-start |
|
|
72
|
+
| `/api/tunnel` | GET/POST | Status / toggle tunnel publik |
|
|
73
|
+
| `/api/donations` | GET | List donasi + statistik (paginasi) |
|
|
74
|
+
| `/api/donations/import` | POST | Import CSV (dedupe by email) |
|
|
75
|
+
| `/api/donations/:id/check-eligibility` | POST | CE + auto refresh token |
|
|
76
|
+
| `/api/donations/:id/sync` | POST | Upsert ke 9Router by email |
|
|
77
|
+
| `/api/generate`, `/api/insert` | POST | Query SQL / insert manual (admin) |
|
|
78
|
+
| `/api/keychain`, `/api/decode` | POST | Ambil token dari Keychain (admin) |
|
|
79
|
+
|
|
80
|
+
## Konvensi & Catatan Teknis
|
|
81
|
+
|
|
82
|
+
- **CommonJS** di backend (`require`/`module.exports`), ESLint flat config di `eslint.config.js` — `npm run lint` harus bersih sebelum commit/publish.
|
|
83
|
+
- **Priority insert 9Router**: selalu `COUNT(provider antigravity + authType oauth) + 1` (urutan terakhir), `isActive = 0`.
|
|
84
|
+
- **Import CSV**: email sama → update hanya jika data baru lebih baru (banding Expiry, lalu CreatedAt).
|
|
85
|
+
- **Tailwind**: content glob di `web/tailwind.config.js` memakai path absolut (`__dirname`) — jangan ubah ke relatif, karena resolve-nya bergantung cwd proses build.
|
|
86
|
+
- **JWT secret**: dibuat random sekali per instalasi, disimpan di config — tidak pernah ikut ter-publish.
|
|
87
|
+
- **Single instance**: lock di `$TMPDIR/antigravity-token-collector.lock`; instance kedua keluar dengan pesan "Sudah berjalan di menubar."
|
|
88
|
+
- Desain frontend: neobrutalist (hard shadow, border hitam tebal, warna flat) — design system ada di `web/src/styles.css`, maskot Astro-G di `web/src/shared/Mascot.vue`.
|
|
89
|
+
|
|
90
|
+
## Testing Manual Cepat
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# login (password default)
|
|
94
|
+
curl -c ck.txt -X POST localhost:3456/api/verify-password \
|
|
95
|
+
-H 'Content-Type: application/json' -d '{"password":"1234567890"}'
|
|
96
|
+
|
|
97
|
+
# settings (harus 200)
|
|
98
|
+
curl -b ck.txt localhost:3456/api/settings
|
|
99
|
+
|
|
100
|
+
# tanpa cookie (harus 401)
|
|
101
|
+
curl localhost:3456/api/donations
|
|
102
|
+
```
|
package/env.example
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
PORT=3456
|
|
2
|
+
APP_PASSWORD=
|
|
3
|
+
DB_PATH=
|
|
4
|
+
|
|
5
|
+
# Opsional — kredensial OAuth sudah tertanam bawaan (memakai kredensial publik
|
|
6
|
+
# Antigravity CLI), jadi biasanya TIDAK perlu diisi. Isi hanya jika ingin
|
|
7
|
+
# memakai OAuth client Anda sendiri:
|
|
8
|
+
# OAUTH_CLIENT_ID=
|
|
9
|
+
# OAUTH_CLIENT_SECRET=
|
|
10
|
+
|
|
11
|
+
# Options: tab (default), new_tab, or popup
|
|
12
|
+
OAUTH_METHOD=popup
|
|
Binary file
|
|
Binary file
|
package/images/Sync.jpg
ADDED
|
Binary file
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
// .env selalu dibaca dari folder aplikasi (bukan cwd) supaya kredensial OAuth
|
|
4
|
+
// tetap tersedia saat dijalankan via npm link / LaunchAgent dari folder mana pun.
|
|
5
|
+
require('dotenv').config({ path: path.join(__dirname, '.env'), quiet: true });
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const lockfile = require('proper-lockfile');
|
|
11
|
+
|
|
12
|
+
const { getConfig } = require('./src/config');
|
|
13
|
+
const { isAutoStartEnabled } = require('./src/autostart');
|
|
14
|
+
const { TrayManager } = require('./src/tray');
|
|
15
|
+
const { showMainMenu } = require('./src/cli');
|
|
16
|
+
|
|
17
|
+
const config = getConfig();
|
|
18
|
+
const configuredPort = Number(process.env.PORT || config.get('port') || 3456);
|
|
19
|
+
process.env.PORT = String(configuredPort);
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
startServer,
|
|
23
|
+
server,
|
|
24
|
+
getTunnelManager,
|
|
25
|
+
} = require('./server');
|
|
26
|
+
|
|
27
|
+
const LOCK_FILE_PATH = path.join(os.tmpdir(), 'antigravity-token-collector.lock');
|
|
28
|
+
|
|
29
|
+
let releaseLock = null;
|
|
30
|
+
let trayManager = null;
|
|
31
|
+
let menuOpen = false;
|
|
32
|
+
let isHandoffInProgress = false;
|
|
33
|
+
|
|
34
|
+
async function acquireSingleInstanceLock() {
|
|
35
|
+
if (!fs.existsSync(LOCK_FILE_PATH)) {
|
|
36
|
+
fs.writeFileSync(LOCK_FILE_PATH, 'lock\n', 'utf8');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
releaseLock = await lockfile.lock(LOCK_FILE_PATH, {
|
|
41
|
+
realpath: false,
|
|
42
|
+
retries: 0,
|
|
43
|
+
});
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.log('Sudah berjalan di menubar.');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function cleanupAndExit(exitCode = 0) {
|
|
51
|
+
try {
|
|
52
|
+
const tunnelManager = getTunnelManager();
|
|
53
|
+
if (tunnelManager) {
|
|
54
|
+
// Matikan cloudflared tanpa mereset tunnelEnabled agar auto-start
|
|
55
|
+
// tunnel tetap bekerja pada launch berikutnya.
|
|
56
|
+
await tunnelManager.dispose();
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (trayManager) {
|
|
62
|
+
await trayManager.stop();
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
if (server.listening) {
|
|
68
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (releaseLock) {
|
|
74
|
+
await releaseLock();
|
|
75
|
+
releaseLock = null;
|
|
76
|
+
}
|
|
77
|
+
} catch (error) {}
|
|
78
|
+
|
|
79
|
+
process.exit(exitCode);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function releaseInstanceLock() {
|
|
83
|
+
if (!releaseLock) return;
|
|
84
|
+
try {
|
|
85
|
+
await releaseLock();
|
|
86
|
+
} catch (error) {}
|
|
87
|
+
releaseLock = null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function closeServer() {
|
|
91
|
+
if (!server.listening) return;
|
|
92
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function handoffToBackground(port) {
|
|
96
|
+
if (isHandoffInProgress) return;
|
|
97
|
+
isHandoffInProgress = true;
|
|
98
|
+
|
|
99
|
+
const appUrl = `http://localhost:${port}`;
|
|
100
|
+
|
|
101
|
+
await closeServer();
|
|
102
|
+
await releaseInstanceLock();
|
|
103
|
+
|
|
104
|
+
const child = spawn(process.execPath, [path.join(__dirname, 'index.js'), '--hidden'], {
|
|
105
|
+
cwd: process.cwd(),
|
|
106
|
+
detached: true,
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
PORT: String(port),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
child.unref();
|
|
114
|
+
|
|
115
|
+
console.log('⏳ Switching to tray mode... (icon already visible in menu bar)');
|
|
116
|
+
console.log(`🔔 Antigravity-tc (PID: ${child.pid})`);
|
|
117
|
+
console.log(` Dashboard: ${appUrl}/dashboard`);
|
|
118
|
+
console.log('');
|
|
119
|
+
console.log('💡 You can close this terminal. Klik icon tray untuk quit.');
|
|
120
|
+
|
|
121
|
+
process.exit(0);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function openTerminalMenu(menuContext) {
|
|
125
|
+
if (menuOpen) return;
|
|
126
|
+
menuOpen = true;
|
|
127
|
+
try {
|
|
128
|
+
await showMainMenu(menuContext);
|
|
129
|
+
} finally {
|
|
130
|
+
menuOpen = false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function main() {
|
|
135
|
+
await acquireSingleInstanceLock();
|
|
136
|
+
|
|
137
|
+
const autoStart = await isAutoStartEnabled().catch(() => false);
|
|
138
|
+
config.set('autoStart', autoStart);
|
|
139
|
+
config.set('port', configuredPort);
|
|
140
|
+
|
|
141
|
+
const port = await startServer(configuredPort);
|
|
142
|
+
const appUrl = `http://localhost:${port}/dashboard`;
|
|
143
|
+
|
|
144
|
+
trayManager = new TrayManager({ config, port });
|
|
145
|
+
await trayManager.start();
|
|
146
|
+
trayManager.on('exit', () => cleanupAndExit(0));
|
|
147
|
+
|
|
148
|
+
const menuContext = {
|
|
149
|
+
trayManager,
|
|
150
|
+
appUrl,
|
|
151
|
+
onHideToTray: () => handoffToBackground(port),
|
|
152
|
+
onExit: () => cleanupAndExit(0),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const hidden = process.argv.includes('--hidden');
|
|
156
|
+
if (!hidden) {
|
|
157
|
+
await openTerminalMenu(menuContext);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
process.on('SIGINT', () => {
|
|
162
|
+
cleanupAndExit(0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
process.on('SIGTERM', () => {
|
|
166
|
+
cleanupAndExit(0);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
process.on('uncaughtException', (error) => {
|
|
170
|
+
console.error('Uncaught exception:', error);
|
|
171
|
+
cleanupAndExit(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
process.on('unhandledRejection', (error) => {
|
|
175
|
+
console.error('Unhandled rejection:', error);
|
|
176
|
+
cleanupAndExit(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
main().catch((error) => {
|
|
180
|
+
console.error('Failed to start:', error);
|
|
181
|
+
cleanupAndExit(1);
|
|
182
|
+
});
|