apertodns 1.0.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/README.md +75 -0
- package/index.js +1125 -0
- package/package.json +51 -0
- package/utils.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ApertoDNS CLI
|
|
2
|
+
|
|
3
|
+
Dynamic DNS management from your terminal. Manage domains, tokens, and DNS updates with style.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g apertodns
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Setup (login or register)
|
|
15
|
+
apertodns --setup
|
|
16
|
+
|
|
17
|
+
# View dashboard
|
|
18
|
+
apertodns --dashboard
|
|
19
|
+
|
|
20
|
+
# List your domains
|
|
21
|
+
apertodns --domains
|
|
22
|
+
|
|
23
|
+
# Add a new domain
|
|
24
|
+
apertodns --add-domain myserver.apertodns.com
|
|
25
|
+
|
|
26
|
+
# Test DNS resolution
|
|
27
|
+
apertodns --test google.com
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
### Main Commands
|
|
33
|
+
| Command | Description |
|
|
34
|
+
|---------|-------------|
|
|
35
|
+
| `--dashboard` | Complete dashboard with all info |
|
|
36
|
+
| `--domains` | List all your domains |
|
|
37
|
+
| `--tokens` | List all your tokens |
|
|
38
|
+
| `--stats` | Statistics and metrics |
|
|
39
|
+
| `--logs` | Recent activity logs |
|
|
40
|
+
|
|
41
|
+
### Domain Management
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---------|-------------|
|
|
44
|
+
| `--add-domain <name>` | Create a new domain |
|
|
45
|
+
| `--delete-domain` | Delete a domain (interactive) |
|
|
46
|
+
| `--test <domain>` | Test DNS resolution |
|
|
47
|
+
|
|
48
|
+
### Token Management
|
|
49
|
+
| Command | Description |
|
|
50
|
+
|---------|-------------|
|
|
51
|
+
| `--enable <id>` | Enable a token |
|
|
52
|
+
| `--disable <id>` | Disable a token |
|
|
53
|
+
| `--toggle <id>` | Toggle token state |
|
|
54
|
+
|
|
55
|
+
### Configuration
|
|
56
|
+
| Command | Description |
|
|
57
|
+
|---------|-------------|
|
|
58
|
+
| `--setup` | Guided setup (login/register) |
|
|
59
|
+
| `--status` | Show current status |
|
|
60
|
+
| `--force` | Force DNS update |
|
|
61
|
+
|
|
62
|
+
## Interactive Mode
|
|
63
|
+
|
|
64
|
+
Run `apertodns` without arguments for interactive menu.
|
|
65
|
+
|
|
66
|
+
## Cron Setup
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Every 5 minutes
|
|
70
|
+
*/5 * * * * apertodns --cron
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## License
|
|
74
|
+
|
|
75
|
+
MIT - Aperto Network
|
package/index.js
ADDED
|
@@ -0,0 +1,1125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import fetch from "node-fetch";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import readline from "readline";
|
|
8
|
+
import inquirer from "inquirer";
|
|
9
|
+
import { log, getCurrentIP, getCurrentIPv6, loadLastIP, saveCurrentIP } from "./utils.js";
|
|
10
|
+
import chalk from "chalk";
|
|
11
|
+
import figlet from "figlet";
|
|
12
|
+
import Table from "cli-table3";
|
|
13
|
+
import ora from "ora";
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CONFIG_PATH = path.resolve(__dirname, "config.json");
|
|
17
|
+
const UPDATE_CACHE_PATH = path.resolve(__dirname, ".update-check");
|
|
18
|
+
const API_BASE = "https://api.apertodns.com/api";
|
|
19
|
+
|
|
20
|
+
// Leggi versione da package.json
|
|
21
|
+
const getPackageVersion = () => {
|
|
22
|
+
try {
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
|
|
24
|
+
return pkg.version;
|
|
25
|
+
} catch {
|
|
26
|
+
return "0.0.0";
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const CURRENT_VERSION = getPackageVersion();
|
|
31
|
+
const UPDATE_CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 ore in ms
|
|
32
|
+
|
|
33
|
+
// Confronto semver robusto
|
|
34
|
+
const compareVersions = (v1, v2) => {
|
|
35
|
+
const parts1 = v1.split('.').map(Number);
|
|
36
|
+
const parts2 = v2.split('.').map(Number);
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
|
39
|
+
const p1 = parts1[i] || 0;
|
|
40
|
+
const p2 = parts2[i] || 0;
|
|
41
|
+
if (p1 > p2) return 1;
|
|
42
|
+
if (p1 < p2) return -1;
|
|
43
|
+
}
|
|
44
|
+
return 0;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Check aggiornamenti con cache
|
|
48
|
+
const checkForUpdates = async () => {
|
|
49
|
+
try {
|
|
50
|
+
// Controlla cache
|
|
51
|
+
if (fs.existsSync(UPDATE_CACHE_PATH)) {
|
|
52
|
+
const cache = JSON.parse(fs.readFileSync(UPDATE_CACHE_PATH, "utf-8"));
|
|
53
|
+
const cacheAge = Date.now() - (cache.timestamp || 0);
|
|
54
|
+
|
|
55
|
+
// Se cache recente, usa il risultato cachato
|
|
56
|
+
if (cacheAge < UPDATE_CHECK_INTERVAL) {
|
|
57
|
+
if (cache.latestVersion && compareVersions(cache.latestVersion, CURRENT_VERSION) > 0) {
|
|
58
|
+
return cache.latestVersion;
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Fetch da npm registry
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
67
|
+
|
|
68
|
+
const res = await fetch("https://registry.npmjs.org/apertodns/latest", {
|
|
69
|
+
signal: controller.signal
|
|
70
|
+
});
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
|
|
73
|
+
if (!res.ok) return null;
|
|
74
|
+
const data = await res.json();
|
|
75
|
+
const latestVersion = data.version;
|
|
76
|
+
|
|
77
|
+
// Salva in cache
|
|
78
|
+
fs.writeFileSync(UPDATE_CACHE_PATH, JSON.stringify({
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
latestVersion,
|
|
81
|
+
checkedVersion: CURRENT_VERSION
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Confronta versioni
|
|
85
|
+
if (latestVersion && compareVersions(latestVersion, CURRENT_VERSION) > 0) {
|
|
86
|
+
return latestVersion;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
} catch {
|
|
90
|
+
return null; // Silenzioso se offline o errore
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Colors
|
|
95
|
+
const orange = chalk.hex('#f97316');
|
|
96
|
+
const green = chalk.hex('#22c55e');
|
|
97
|
+
const red = chalk.hex('#ef4444');
|
|
98
|
+
const blue = chalk.hex('#3b82f6');
|
|
99
|
+
const purple = chalk.hex('#a855f7');
|
|
100
|
+
const yellow = chalk.hex('#eab308');
|
|
101
|
+
const gray = chalk.hex('#71717a');
|
|
102
|
+
const cyan = chalk.hex('#06b6d4');
|
|
103
|
+
|
|
104
|
+
const showBanner = async () => {
|
|
105
|
+
console.clear();
|
|
106
|
+
const banner = figlet.textSync("ApertoDNS", { font: "Standard" });
|
|
107
|
+
console.log(orange(banner));
|
|
108
|
+
console.log(gray(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
109
|
+
console.log(gray(" ā") + cyan(` ApertoDNS CLI v${CURRENT_VERSION}`) + gray(" - Dynamic DNS Reinvented ā"));
|
|
110
|
+
console.log(gray(" ā") + gray(" Gestisci domini, token e DNS dalla tua shell. ā"));
|
|
111
|
+
console.log(gray(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n"));
|
|
112
|
+
|
|
113
|
+
// Check aggiornamenti in background
|
|
114
|
+
const newVersion = await checkForUpdates();
|
|
115
|
+
if (newVersion) {
|
|
116
|
+
console.log(yellow(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"));
|
|
117
|
+
console.log(yellow(" ā") + red(" ā ļø Nuova versione disponibile: ") + green.bold(`v${newVersion}`) + yellow(" ā"));
|
|
118
|
+
console.log(yellow(" ā") + gray(` Tu hai: v${CURRENT_VERSION}`) + yellow(" ā"));
|
|
119
|
+
console.log(yellow(" ā") + cyan(" Aggiorna: ") + chalk.white("npm update -g apertodns-cli") + yellow(" ā"));
|
|
120
|
+
console.log(yellow(" āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n"));
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const promptInput = (question) => {
|
|
125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
return new Promise((resolve) => rl.question(question, (ans) => { rl.close(); resolve(ans.trim()); }));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Parse args
|
|
130
|
+
const args = process.argv.slice(2);
|
|
131
|
+
const isCron = args.includes("--cron");
|
|
132
|
+
if (isCron) {
|
|
133
|
+
process.argv.push("--quiet");
|
|
134
|
+
process.argv.push("--json");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const isQuiet = args.includes("--quiet");
|
|
138
|
+
const showHelp = args.includes("--help");
|
|
139
|
+
const showVersion = args.includes("--version");
|
|
140
|
+
const showJson = args.includes("--json");
|
|
141
|
+
const runVerify = args.includes("--verify");
|
|
142
|
+
const runSetup = args.includes("--setup");
|
|
143
|
+
const showStatus = args.includes("--status") || args.includes("--show");
|
|
144
|
+
const forceUpdate = args.includes("--force");
|
|
145
|
+
const enableTokenId = args.includes("--enable") ? args[args.indexOf("--enable") + 1] : null;
|
|
146
|
+
const disableTokenId = args.includes("--disable") ? args[args.indexOf("--disable") + 1] : null;
|
|
147
|
+
const toggleTokenId = args.includes("--toggle") ? args[args.indexOf("--toggle") + 1] : null;
|
|
148
|
+
const runConfigEdit = args.includes("--config");
|
|
149
|
+
const listDomains = args.includes("--domains");
|
|
150
|
+
const listTokens = args.includes("--tokens");
|
|
151
|
+
const addDomainArg = args.includes("--add-domain") ? args[args.indexOf("--add-domain") + 1] : null;
|
|
152
|
+
const deleteDomainArg = args.includes("--delete-domain") ? args[args.indexOf("--delete-domain") + 1] : null;
|
|
153
|
+
const showStats = args.includes("--stats");
|
|
154
|
+
const showLogs = args.includes("--logs");
|
|
155
|
+
const testDns = args.includes("--test") ? args[args.indexOf("--test") + 1] : null;
|
|
156
|
+
const showDashboard = args.includes("--dashboard");
|
|
157
|
+
const listWebhooks = args.includes("--webhooks");
|
|
158
|
+
const listApiKeys = args.includes("--api-keys");
|
|
159
|
+
const runInteractive = args.length === 0;
|
|
160
|
+
|
|
161
|
+
// Show help
|
|
162
|
+
if (showHelp) {
|
|
163
|
+
console.log(`
|
|
164
|
+
${orange.bold("ApertoDNS CLI")} - Gestisci il tuo DNS dinamico
|
|
165
|
+
|
|
166
|
+
${chalk.bold("USAGE:")}
|
|
167
|
+
apertodns [command] [options]
|
|
168
|
+
|
|
169
|
+
${chalk.bold("COMANDI PRINCIPALI:")}
|
|
170
|
+
${cyan("--dashboard")} Dashboard completa con tutte le info
|
|
171
|
+
${cyan("--domains")} Lista tutti i tuoi domini (tabella)
|
|
172
|
+
${cyan("--tokens")} Lista tutti i tuoi token (tabella)
|
|
173
|
+
${cyan("--stats")} Statistiche e metriche
|
|
174
|
+
${cyan("--logs")} Ultimi log di attivitĆ
|
|
175
|
+
|
|
176
|
+
${chalk.bold("GESTIONE DOMINI:")}
|
|
177
|
+
${cyan("--add-domain")} <name> Crea un nuovo dominio
|
|
178
|
+
${cyan("--delete-domain")} Elimina un dominio (interattivo)
|
|
179
|
+
${cyan("--test")} <domain> Testa risoluzione DNS di un dominio
|
|
180
|
+
|
|
181
|
+
${chalk.bold("GESTIONE TOKEN:")}
|
|
182
|
+
${cyan("--enable")} <id> Attiva un token
|
|
183
|
+
${cyan("--disable")} <id> Disattiva un token
|
|
184
|
+
${cyan("--toggle")} <id> Inverte stato token (ON/OFF)
|
|
185
|
+
${cyan("--verify")} Verifica validitĆ token
|
|
186
|
+
|
|
187
|
+
${chalk.bold("INTEGRAZIONI:")}
|
|
188
|
+
${cyan("--webhooks")} Lista webhook configurati
|
|
189
|
+
${cyan("--api-keys")} Lista API keys
|
|
190
|
+
|
|
191
|
+
${chalk.bold("CONFIGURAZIONE:")}
|
|
192
|
+
${cyan("--setup")} Configurazione guidata (login/registrazione)
|
|
193
|
+
${cyan("--status")} Mostra stato attuale
|
|
194
|
+
${cyan("--config")} Modifica configurazione
|
|
195
|
+
${cyan("--force")} Forza aggiornamento DNS
|
|
196
|
+
|
|
197
|
+
${chalk.bold("OPZIONI:")}
|
|
198
|
+
${cyan("--cron")} ModalitĆ silenziosa per cronjob
|
|
199
|
+
${cyan("--quiet")} Nasconde banner
|
|
200
|
+
${cyan("--json")} Output JSON
|
|
201
|
+
${cyan("--version")} Mostra versione
|
|
202
|
+
${cyan("--help")} Mostra questo help
|
|
203
|
+
|
|
204
|
+
${chalk.bold("MODALITĆ INTERATTIVA:")}
|
|
205
|
+
Esegui ${cyan("apertodns")} senza argomenti per il menu interattivo.
|
|
206
|
+
|
|
207
|
+
${gray("Esempi:")}
|
|
208
|
+
${gray("$")} apertodns --dashboard
|
|
209
|
+
${gray("$")} apertodns --domains
|
|
210
|
+
${gray("$")} apertodns --add-domain mioserver.apertodns.com
|
|
211
|
+
${gray("$")} apertodns --test mioserver.apertodns.com
|
|
212
|
+
${gray("$")} apertodns --stats
|
|
213
|
+
`);
|
|
214
|
+
process.exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (showVersion) {
|
|
218
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, "package.json"), "utf-8"));
|
|
219
|
+
console.log(`ApertoDNS CLI v${pkg.version}`);
|
|
220
|
+
process.exit(0);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (!isQuiet && !isCron) showBanner();
|
|
224
|
+
|
|
225
|
+
// Load config
|
|
226
|
+
let config = {};
|
|
227
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
228
|
+
try {
|
|
229
|
+
config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
console.error(red("Errore lettura config.json:"), err.message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Helper: get JWT token (per API: domains, tokens, dashboard...)
|
|
236
|
+
const getAuthToken = async () => {
|
|
237
|
+
if (config.jwtToken) return config.jwtToken;
|
|
238
|
+
if (config.apiToken) return config.apiToken; // backward compatibility
|
|
239
|
+
return await promptInput(cyan("š Token JWT: "));
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Helper: get CLI token (per DDNS: status, force, update...)
|
|
243
|
+
const getCliToken = async () => {
|
|
244
|
+
if (config.cliToken) return config.cliToken;
|
|
245
|
+
if (config.apiToken) return config.apiToken; // backward compatibility
|
|
246
|
+
return await promptInput(cyan("š Token CLI: "));
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Helper: create spinner
|
|
250
|
+
const spinner = (text) => ora({ text, spinner: "dots", color: "yellow" });
|
|
251
|
+
|
|
252
|
+
// ==================== DOMAINS ====================
|
|
253
|
+
|
|
254
|
+
const fetchDomains = async () => {
|
|
255
|
+
const token = await getAuthToken();
|
|
256
|
+
const spin = spinner("Caricamento domini...").start();
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(`${API_BASE}/domains`, {
|
|
259
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
260
|
+
});
|
|
261
|
+
spin.stop();
|
|
262
|
+
if (!res.ok) throw new Error("Errore fetch domini");
|
|
263
|
+
return await res.json();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
spin.fail("Errore caricamento domini");
|
|
266
|
+
return [];
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const showDomainsList = async () => {
|
|
271
|
+
const domains = await fetchDomains();
|
|
272
|
+
if (domains.length === 0) {
|
|
273
|
+
console.log(yellow("\nā ļø Nessun dominio trovato.\n"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const table = new Table({
|
|
278
|
+
head: [
|
|
279
|
+
gray('STATO'),
|
|
280
|
+
orange.bold('DOMINIO'),
|
|
281
|
+
cyan('IP ATTUALE'),
|
|
282
|
+
gray('TTL'),
|
|
283
|
+
gray('ULTIMO UPDATE')
|
|
284
|
+
],
|
|
285
|
+
style: { head: [], border: ['gray'] },
|
|
286
|
+
chars: {
|
|
287
|
+
'top': 'ā', 'top-mid': 'ā¬', 'top-left': 'ā', 'top-right': 'ā',
|
|
288
|
+
'bottom': 'ā', 'bottom-mid': 'ā“', 'bottom-left': 'ā', 'bottom-right': 'ā',
|
|
289
|
+
'left': 'ā', 'left-mid': 'ā', 'mid': 'ā', 'mid-mid': 'ā¼',
|
|
290
|
+
'right': 'ā', 'right-mid': 'ā¤', 'middle': 'ā'
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
domains.forEach(d => {
|
|
295
|
+
const status = d.currentIp ? green('ā ONLINE') : red('ā OFFLINE');
|
|
296
|
+
const lastUpdate = d.lastUpdated
|
|
297
|
+
? new Date(d.lastUpdated).toLocaleString("it-IT", { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
298
|
+
: gray('Mai');
|
|
299
|
+
|
|
300
|
+
table.push([
|
|
301
|
+
status,
|
|
302
|
+
chalk.bold(d.name),
|
|
303
|
+
d.currentIp || gray('N/D'),
|
|
304
|
+
`${d.ttl}s`,
|
|
305
|
+
lastUpdate
|
|
306
|
+
]);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
console.log(`\nš ${chalk.bold('I tuoi domini')} (${domains.length})\n`);
|
|
310
|
+
console.log(table.toString());
|
|
311
|
+
console.log();
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const addDomain = async (name) => {
|
|
315
|
+
const token = await getAuthToken();
|
|
316
|
+
const domainName = name || await promptInput(cyan("š Nome dominio (es. mioserver.apertodns.com): "));
|
|
317
|
+
|
|
318
|
+
if (!domainName) {
|
|
319
|
+
console.log(red("Nome dominio richiesto."));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const spin = spinner(`Creazione dominio ${domainName}...`).start();
|
|
324
|
+
try {
|
|
325
|
+
const res = await fetch(`${API_BASE}/domains/standard`, {
|
|
326
|
+
method: "POST",
|
|
327
|
+
headers: {
|
|
328
|
+
"Content-Type": "application/json",
|
|
329
|
+
Authorization: `Bearer ${token}`
|
|
330
|
+
},
|
|
331
|
+
body: JSON.stringify({ name: domainName })
|
|
332
|
+
});
|
|
333
|
+
const data = await res.json();
|
|
334
|
+
|
|
335
|
+
if (res.ok) {
|
|
336
|
+
spin.succeed(`Dominio "${domainName}" creato!`);
|
|
337
|
+
if (data.token) {
|
|
338
|
+
console.log(yellow("\nš Token generato:"), chalk.bold.white(data.token));
|
|
339
|
+
console.log(gray(" (Salvalo subito, non sarà più visibile)\n"));
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
spin.fail(`Errore: ${data.error || data.message}`);
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
spin.fail(err.message);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const deleteDomain = async (name) => {
|
|
350
|
+
const token = await getAuthToken();
|
|
351
|
+
const domains = await fetchDomains();
|
|
352
|
+
|
|
353
|
+
let domainName = name;
|
|
354
|
+
if (!domainName) {
|
|
355
|
+
if (domains.length === 0) {
|
|
356
|
+
console.log(yellow("Nessun dominio da eliminare."));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
const { selected } = await inquirer.prompt([{
|
|
360
|
+
type: "list",
|
|
361
|
+
name: "selected",
|
|
362
|
+
message: "Quale dominio vuoi eliminare?",
|
|
363
|
+
choices: domains.map(d => ({
|
|
364
|
+
name: `${d.currentIp ? green('ā') : red('ā')} ${d.name}`,
|
|
365
|
+
value: d.name
|
|
366
|
+
}))
|
|
367
|
+
}]);
|
|
368
|
+
domainName = selected;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const domain = domains.find(d => d.name === domainName);
|
|
372
|
+
if (!domain) {
|
|
373
|
+
console.log(red(`Dominio "${domainName}" non trovato.`));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { confirm } = await inquirer.prompt([{
|
|
378
|
+
type: "confirm",
|
|
379
|
+
name: "confirm",
|
|
380
|
+
message: red(`ā ļø Eliminare definitivamente "${domainName}"?`),
|
|
381
|
+
default: false
|
|
382
|
+
}]);
|
|
383
|
+
|
|
384
|
+
if (!confirm) {
|
|
385
|
+
console.log(gray("Operazione annullata."));
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const spin = spinner(`Eliminazione ${domainName}...`).start();
|
|
390
|
+
try {
|
|
391
|
+
const res = await fetch(`${API_BASE}/domains/${domain.id}`, {
|
|
392
|
+
method: "DELETE",
|
|
393
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (res.ok) {
|
|
397
|
+
spin.succeed(`Dominio "${domainName}" eliminato.`);
|
|
398
|
+
} else {
|
|
399
|
+
const data = await res.json();
|
|
400
|
+
spin.fail(`Errore: ${data.error || data.message}`);
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
spin.fail(err.message);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
// ==================== DNS TEST ====================
|
|
408
|
+
|
|
409
|
+
const testDnsResolution = async (domain) => {
|
|
410
|
+
const domainToTest = domain || await promptInput(cyan("š Dominio da testare: "));
|
|
411
|
+
if (!domainToTest) return;
|
|
412
|
+
|
|
413
|
+
// Validazione dominio per prevenire command injection
|
|
414
|
+
const domainRegex = /^[a-zA-Z0-9][a-zA-Z0-9-_.]+[a-zA-Z0-9]$/;
|
|
415
|
+
if (!domainRegex.test(domainToTest) || domainToTest.includes('..')) {
|
|
416
|
+
console.log(red("\nā Nome dominio non valido.\n"));
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const spin = spinner(`Testing DNS per ${domainToTest}...`).start();
|
|
421
|
+
|
|
422
|
+
try {
|
|
423
|
+
const { execFileSync } = await import('child_process');
|
|
424
|
+
const result = execFileSync('dig', ['+short', domainToTest, 'A'], { encoding: 'utf-8' }).trim();
|
|
425
|
+
const result6 = execFileSync('dig', ['+short', domainToTest, 'AAAA'], { encoding: 'utf-8' }).trim();
|
|
426
|
+
|
|
427
|
+
spin.stop();
|
|
428
|
+
console.log(`\nš ${chalk.bold('Risultati DNS per')} ${cyan(domainToTest)}\n`);
|
|
429
|
+
|
|
430
|
+
const table = new Table({
|
|
431
|
+
style: { head: [], border: ['gray'] }
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
table.push(
|
|
435
|
+
[gray('Record A (IPv4)'), result || red('Non trovato')],
|
|
436
|
+
[gray('Record AAAA (IPv6)'), result6 || gray('Non configurato')]
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
console.log(table.toString());
|
|
440
|
+
|
|
441
|
+
// Propagation check
|
|
442
|
+
console.log(`\n${gray('Propagazione DNS:')}`);
|
|
443
|
+
const dnsServers = ['8.8.8.8', '1.1.1.1'];
|
|
444
|
+
for (const dns of dnsServers) {
|
|
445
|
+
try {
|
|
446
|
+
const check = execFileSync('dig', ['+short', `@${dns}`, domainToTest, 'A'], { encoding: 'utf-8' }).trim();
|
|
447
|
+
console.log(` ${dns}: ${check ? green('ā ' + check) : red('ā Non trovato')}`);
|
|
448
|
+
} catch {
|
|
449
|
+
console.log(` ${dns}: ${red('ā Errore')}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
console.log();
|
|
453
|
+
} catch (err) {
|
|
454
|
+
spin.fail("Errore nel test DNS");
|
|
455
|
+
console.log(gray(" (Assicurati che 'dig' sia installato)\n"));
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// ==================== TOKENS ====================
|
|
460
|
+
|
|
461
|
+
const fetchTokens = async () => {
|
|
462
|
+
const token = await getAuthToken();
|
|
463
|
+
const spin = spinner("Caricamento token...").start();
|
|
464
|
+
try {
|
|
465
|
+
const res = await fetch(`${API_BASE}/tokens`, {
|
|
466
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
467
|
+
});
|
|
468
|
+
spin.stop();
|
|
469
|
+
if (!res.ok) throw new Error("Errore fetch token");
|
|
470
|
+
return await res.json();
|
|
471
|
+
} catch (err) {
|
|
472
|
+
spin.fail("Errore caricamento token");
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
const showTokensList = async () => {
|
|
478
|
+
const tokens = await fetchTokens();
|
|
479
|
+
if (tokens.length === 0) {
|
|
480
|
+
console.log(yellow("\nā ļø Nessun token trovato.\n"));
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const table = new Table({
|
|
485
|
+
head: [
|
|
486
|
+
gray('STATO'),
|
|
487
|
+
orange.bold('ETICHETTA'),
|
|
488
|
+
cyan('DOMINIO'),
|
|
489
|
+
gray('ID'),
|
|
490
|
+
gray('ULTIMO USO')
|
|
491
|
+
],
|
|
492
|
+
style: { head: [], border: ['gray'] }
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
tokens.forEach(t => {
|
|
496
|
+
const status = t.active ? green('ā ATTIVO') : red('ā OFF');
|
|
497
|
+
const lastUsed = t.lastUsed
|
|
498
|
+
? new Date(t.lastUsed).toLocaleString("it-IT", { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
499
|
+
: gray('Mai');
|
|
500
|
+
|
|
501
|
+
table.push([
|
|
502
|
+
status,
|
|
503
|
+
chalk.bold(t.label || 'N/D'),
|
|
504
|
+
t.domain?.name || gray('N/D'),
|
|
505
|
+
gray(t.id),
|
|
506
|
+
lastUsed
|
|
507
|
+
]);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
console.log(`\nš ${chalk.bold('I tuoi token')} (${tokens.length})\n`);
|
|
511
|
+
console.log(table.toString());
|
|
512
|
+
console.log();
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const updateTokenState = async (tokenId, desiredState = null) => {
|
|
516
|
+
const apiToken = await getAuthToken();
|
|
517
|
+
if (!tokenId) {
|
|
518
|
+
console.error(red("Devi specificare un tokenId"));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
let finalState = desiredState;
|
|
523
|
+
if (desiredState === null) {
|
|
524
|
+
const spin = spinner("Caricamento...").start();
|
|
525
|
+
const res = await fetch(`${API_BASE}/tokens`, {
|
|
526
|
+
headers: { Authorization: `Bearer ${apiToken}` }
|
|
527
|
+
});
|
|
528
|
+
const all = await res.json();
|
|
529
|
+
spin.stop();
|
|
530
|
+
|
|
531
|
+
const token = all.find(t => t.id === parseInt(tokenId));
|
|
532
|
+
if (!token) {
|
|
533
|
+
console.error(red(`Token ID ${tokenId} non trovato.`));
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
finalState = !token.active;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const spin = spinner(`${finalState ? 'Attivazione' : 'Disattivazione'} token...`).start();
|
|
540
|
+
const res = await fetch(`${API_BASE}/tokens/${tokenId}`, {
|
|
541
|
+
method: "PATCH",
|
|
542
|
+
headers: {
|
|
543
|
+
"Content-Type": "application/json",
|
|
544
|
+
Authorization: `Bearer ${apiToken}`
|
|
545
|
+
},
|
|
546
|
+
body: JSON.stringify({ active: finalState })
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
if (res.ok) {
|
|
550
|
+
spin.succeed(`Token ${tokenId} ${finalState ? green('attivato') : red('disattivato')}`);
|
|
551
|
+
} else {
|
|
552
|
+
const data = await res.json();
|
|
553
|
+
spin.fail(`Errore: ${data.error || data.message}`);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
// ==================== STATS ====================
|
|
558
|
+
|
|
559
|
+
const showStatsCommand = async () => {
|
|
560
|
+
const token = await getAuthToken();
|
|
561
|
+
const spin = spinner("Caricamento statistiche...").start();
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const [domainsRes, tokensRes, statsRes] = await Promise.all([
|
|
565
|
+
fetch(`${API_BASE}/domains`, { headers: { Authorization: `Bearer ${token}` } }),
|
|
566
|
+
fetch(`${API_BASE}/tokens`, { headers: { Authorization: `Bearer ${token}` } }),
|
|
567
|
+
fetch(`${API_BASE}/stats/daily?days=7`, { headers: { Authorization: `Bearer ${token}` } }).catch(() => null)
|
|
568
|
+
]);
|
|
569
|
+
|
|
570
|
+
const domains = await domainsRes.json();
|
|
571
|
+
const tokens = await tokensRes.json();
|
|
572
|
+
const stats = statsRes?.ok ? await statsRes.json() : [];
|
|
573
|
+
|
|
574
|
+
spin.stop();
|
|
575
|
+
|
|
576
|
+
console.log(`\nš ${chalk.bold('Statistiche ApertoDNS')}\n`);
|
|
577
|
+
|
|
578
|
+
// Summary boxes
|
|
579
|
+
const box1 = `āāāāāāāāāāāāāāāāāāā
|
|
580
|
+
ā ${orange.bold(String(domains.length).padStart(2))} Domini ā
|
|
581
|
+
āāāāāāāāāāāāāāāāāāā`;
|
|
582
|
+
|
|
583
|
+
const box2 = `āāāāāāāāāāāāāāāāāāā
|
|
584
|
+
ā ${green.bold(String(tokens.filter(t => t.active).length).padStart(2))} Token attiviā
|
|
585
|
+
āāāāāāāāāāāāāāāāāāā`;
|
|
586
|
+
|
|
587
|
+
const box3 = `āāāāāāāāāāāāāāāāāāā
|
|
588
|
+
ā ${cyan.bold(String(domains.filter(d => d.currentIp).length).padStart(2))} Online ā
|
|
589
|
+
āāāāāāāāāāāāāāāāāāā`;
|
|
590
|
+
|
|
591
|
+
console.log(gray(box1.split('\n')[0] + ' ' + box2.split('\n')[0] + ' ' + box3.split('\n')[0]));
|
|
592
|
+
console.log(gray(box1.split('\n')[1] + ' ' + box2.split('\n')[1] + ' ' + box3.split('\n')[1]));
|
|
593
|
+
console.log(gray(box1.split('\n')[2] + ' ' + box2.split('\n')[2] + ' ' + box3.split('\n')[2]));
|
|
594
|
+
|
|
595
|
+
// Weekly chart
|
|
596
|
+
if (stats.length > 0) {
|
|
597
|
+
console.log(`\n${gray('Aggiornamenti ultimi 7 giorni:')}`);
|
|
598
|
+
const maxUpdates = Math.max(...stats.map(s => s.updates || 0), 1);
|
|
599
|
+
|
|
600
|
+
stats.forEach(day => {
|
|
601
|
+
const barLength = Math.round((day.updates || 0) / maxUpdates * 20);
|
|
602
|
+
const bar = 'ā'.repeat(barLength) + 'ā'.repeat(20 - barLength);
|
|
603
|
+
const date = new Date(day.date).toLocaleDateString('it-IT', { weekday: 'short', day: '2-digit' });
|
|
604
|
+
console.log(` ${gray(date)} ${orange(bar)} ${day.updates || 0}`);
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
console.log();
|
|
609
|
+
} catch (err) {
|
|
610
|
+
spin.fail("Errore caricamento statistiche");
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// ==================== LOGS ====================
|
|
615
|
+
|
|
616
|
+
const showLogsCommand = async () => {
|
|
617
|
+
const token = await getAuthToken();
|
|
618
|
+
const spin = spinner("Caricamento log...").start();
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const res = await fetch(`${API_BASE}/logs?limit=10`, {
|
|
622
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
if (!res.ok) throw new Error("Errore fetch logs");
|
|
626
|
+
const data = await res.json();
|
|
627
|
+
const logs = data.logs || data;
|
|
628
|
+
|
|
629
|
+
spin.stop();
|
|
630
|
+
|
|
631
|
+
if (logs.length === 0) {
|
|
632
|
+
console.log(yellow("\nā ļø Nessun log recente.\n"));
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
console.log(`\nš ${chalk.bold('Ultimi log')}\n`);
|
|
637
|
+
|
|
638
|
+
logs.forEach(l => {
|
|
639
|
+
const time = new Date(l.createdAt).toLocaleString('it-IT', {
|
|
640
|
+
day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
|
|
641
|
+
});
|
|
642
|
+
const actionColor = l.action === 'UPDATE' ? green :
|
|
643
|
+
l.action === 'CREATE' ? blue :
|
|
644
|
+
l.action === 'DELETE' ? red : gray;
|
|
645
|
+
|
|
646
|
+
console.log(` ${gray(time)} ${actionColor(l.action.padEnd(8))} ${l.token?.label || 'N/D'} ${gray('ā')} ${l.token?.domain?.name || 'N/D'}`);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
console.log();
|
|
650
|
+
} catch (err) {
|
|
651
|
+
spin.fail("Errore caricamento log");
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
// ==================== WEBHOOKS ====================
|
|
656
|
+
|
|
657
|
+
const showWebhooksList = async () => {
|
|
658
|
+
const token = await getAuthToken();
|
|
659
|
+
const spin = spinner("Caricamento webhook...").start();
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
const res = await fetch(`${API_BASE}/webhooks`, {
|
|
663
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
if (!res.ok) throw new Error("Errore fetch webhooks");
|
|
667
|
+
const webhooks = await res.json();
|
|
668
|
+
|
|
669
|
+
spin.stop();
|
|
670
|
+
|
|
671
|
+
if (webhooks.length === 0) {
|
|
672
|
+
console.log(yellow("\nā ļø Nessun webhook configurato.\n"));
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const table = new Table({
|
|
677
|
+
head: [gray('STATO'), purple.bold('URL'), gray('EVENTI'), gray('DOMINIO')],
|
|
678
|
+
style: { head: [], border: ['gray'] }
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
webhooks.forEach(w => {
|
|
682
|
+
const status = w.active ? green('ā ON') : red('ā OFF');
|
|
683
|
+
const url = w.url.length > 40 ? w.url.substring(0, 37) + '...' : w.url;
|
|
684
|
+
table.push([status, url, w.events?.join(', ') || 'ALL', w.domain?.name || 'Tutti']);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
console.log(`\nš ${chalk.bold('Webhooks')} (${webhooks.length})\n`);
|
|
688
|
+
console.log(table.toString());
|
|
689
|
+
console.log();
|
|
690
|
+
} catch (err) {
|
|
691
|
+
spin.fail("Errore caricamento webhooks");
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// ==================== API KEYS ====================
|
|
696
|
+
|
|
697
|
+
const showApiKeysList = async () => {
|
|
698
|
+
const token = await getAuthToken();
|
|
699
|
+
const spin = spinner("Caricamento API keys...").start();
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const res = await fetch(`${API_BASE}/api-keys`, {
|
|
703
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
if (!res.ok) throw new Error("Errore fetch API keys");
|
|
707
|
+
const keys = await res.json();
|
|
708
|
+
|
|
709
|
+
spin.stop();
|
|
710
|
+
|
|
711
|
+
if (keys.length === 0) {
|
|
712
|
+
console.log(yellow("\nā ļø Nessuna API key trovata.\n"));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const table = new Table({
|
|
717
|
+
head: [gray('STATO'), blue.bold('NOME'), gray('PREFIX'), gray('SCOPES'), gray('RATE')],
|
|
718
|
+
style: { head: [], border: ['gray'] }
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
keys.forEach(k => {
|
|
722
|
+
const status = k.active ? green('ā ON') : red('ā OFF');
|
|
723
|
+
const scopes = k.scopes?.length > 3 ? `${k.scopes.length} scopes` : k.scopes?.join(', ') || 'N/D';
|
|
724
|
+
table.push([status, chalk.bold(k.name), gray(k.keyPrefix + '...'), scopes, `${k.rateLimit}/h`]);
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
console.log(`\nš ${chalk.bold('API Keys')} (${keys.length})\n`);
|
|
728
|
+
console.log(table.toString());
|
|
729
|
+
console.log();
|
|
730
|
+
} catch (err) {
|
|
731
|
+
spin.fail("Errore caricamento API keys");
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
// ==================== DASHBOARD ====================
|
|
736
|
+
|
|
737
|
+
const showDashboardCommand = async () => {
|
|
738
|
+
const token = await getAuthToken();
|
|
739
|
+
const spin = spinner("Caricamento dashboard...").start();
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const [domainsRes, tokensRes, ipRes] = await Promise.all([
|
|
743
|
+
fetch(`${API_BASE}/domains`, { headers: { Authorization: `Bearer ${token}` } }),
|
|
744
|
+
fetch(`${API_BASE}/tokens`, { headers: { Authorization: `Bearer ${token}` } }),
|
|
745
|
+
getCurrentIP('https://api.ipify.org').catch(() => 'N/D')
|
|
746
|
+
]);
|
|
747
|
+
|
|
748
|
+
const domains = await domainsRes.json();
|
|
749
|
+
const tokens = await tokensRes.json();
|
|
750
|
+
|
|
751
|
+
spin.stop();
|
|
752
|
+
|
|
753
|
+
console.log(`\n${orange.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā')}`);
|
|
754
|
+
console.log(`${orange.bold(' DASHBOARD ')}`);
|
|
755
|
+
console.log(`${orange.bold('āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā')}\n`);
|
|
756
|
+
|
|
757
|
+
// Current IP
|
|
758
|
+
console.log(` š ${gray('IP Attuale:')} ${green.bold(ipRes)}`);
|
|
759
|
+
console.log();
|
|
760
|
+
|
|
761
|
+
// Stats row
|
|
762
|
+
const onlineDomains = domains.filter(d => d.currentIp).length;
|
|
763
|
+
const activeTokens = tokens.filter(t => t.active).length;
|
|
764
|
+
|
|
765
|
+
console.log(` āāāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā¬āāāāāāāāāāāāāāā`);
|
|
766
|
+
console.log(` ā ${orange.bold('DOMINI')} ā ${green.bold('ONLINE')} ā ${cyan.bold('TOKEN')} ā ${purple.bold('ATTIVI')} ā`);
|
|
767
|
+
console.log(` ā ${chalk.bold(String(domains.length).padStart(3))} ā ${chalk.bold(String(onlineDomains).padStart(3))} ā ${chalk.bold(String(tokens.length).padStart(3))} ā ${chalk.bold(String(activeTokens).padStart(3))} ā`);
|
|
768
|
+
console.log(` āāāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā“āāāāāāāāāāāāāāā`);
|
|
769
|
+
console.log();
|
|
770
|
+
|
|
771
|
+
// Domains preview
|
|
772
|
+
if (domains.length > 0) {
|
|
773
|
+
console.log(` ${gray('Ultimi domini:')}`);
|
|
774
|
+
domains.slice(0, 5).forEach(d => {
|
|
775
|
+
const status = d.currentIp ? green('ā') : red('ā');
|
|
776
|
+
console.log(` ${status} ${chalk.bold(d.name)} ${gray('ā')} ${d.currentIp || gray('N/D')}`);
|
|
777
|
+
});
|
|
778
|
+
if (domains.length > 5) console.log(` ${gray(`... e altri ${domains.length - 5}`)}`);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
console.log();
|
|
782
|
+
console.log(` ${gray('ā'.repeat(60))}`);
|
|
783
|
+
console.log(` ${gray('Usa')} ${cyan('--help')} ${gray('per vedere tutti i comandi disponibili')}`);
|
|
784
|
+
console.log();
|
|
785
|
+
} catch (err) {
|
|
786
|
+
spin.fail("Errore caricamento dashboard");
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
// ==================== EXISTING FUNCTIONS ====================
|
|
791
|
+
|
|
792
|
+
const fetchRemoteConfig = async (token) => {
|
|
793
|
+
try {
|
|
794
|
+
const res = await fetch(`${API_BASE}/cli-config/from-token`, {
|
|
795
|
+
method: "POST",
|
|
796
|
+
headers: { "Content-Type": "application/json" },
|
|
797
|
+
body: JSON.stringify({ token })
|
|
798
|
+
});
|
|
799
|
+
const data = await res.json();
|
|
800
|
+
if (!res.ok || !data.config || !data.config.id) throw new Error(data.error || "Configurazione non valida");
|
|
801
|
+
return data.config;
|
|
802
|
+
} catch (err) {
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const setup = async () => {
|
|
808
|
+
console.log(cyan("\nš§ Configurazione ApertoDNS CLI\n"));
|
|
809
|
+
|
|
810
|
+
const { hasAccount } = await inquirer.prompt([{
|
|
811
|
+
type: "list",
|
|
812
|
+
name: "hasAccount",
|
|
813
|
+
message: "Seleziona un'opzione:",
|
|
814
|
+
choices: [
|
|
815
|
+
{ name: green('š Ho giĆ un account - Login'), value: true },
|
|
816
|
+
{ name: blue('š Sono nuovo - Registrati sul sito'), value: false }
|
|
817
|
+
]
|
|
818
|
+
}]);
|
|
819
|
+
|
|
820
|
+
let apiToken;
|
|
821
|
+
|
|
822
|
+
if (hasAccount) {
|
|
823
|
+
const { email, password } = await inquirer.prompt([
|
|
824
|
+
{ type: "input", name: "email", message: "š§ Email:" },
|
|
825
|
+
{ type: "password", name: "password", message: "š Password:", mask: "ā" }
|
|
826
|
+
]);
|
|
827
|
+
|
|
828
|
+
const spin = spinner("Login in corso...").start();
|
|
829
|
+
const res = await fetch(`${API_BASE}/auth/login`, {
|
|
830
|
+
method: "POST",
|
|
831
|
+
headers: { "Content-Type": "application/json" },
|
|
832
|
+
body: JSON.stringify({ email, password })
|
|
833
|
+
});
|
|
834
|
+
const data = await res.json();
|
|
835
|
+
|
|
836
|
+
if (!res.ok || !data.cliToken) {
|
|
837
|
+
spin.fail("Login fallito: " + (data.message || "Errore"));
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
apiToken = data.cliToken;
|
|
841
|
+
spin.succeed("Login effettuato!");
|
|
842
|
+
} else {
|
|
843
|
+
// Registrazione solo via web per sicurezza (captcha)
|
|
844
|
+
console.log(cyan("\nš Per registrarti, visita: ") + orange.bold("https://apertodns.com/register"));
|
|
845
|
+
console.log(gray(" Dopo la registrazione, torna qui per fare il login.\n"));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const remoteConfig = await fetchRemoteConfig(apiToken);
|
|
850
|
+
config = remoteConfig ? { ...remoteConfig, apiToken } : { apiToken };
|
|
851
|
+
|
|
852
|
+
const { save } = await inquirer.prompt([{
|
|
853
|
+
type: "confirm",
|
|
854
|
+
name: "save",
|
|
855
|
+
message: "š¾ Salvare la configurazione su questo computer?",
|
|
856
|
+
default: true
|
|
857
|
+
}]);
|
|
858
|
+
|
|
859
|
+
if (save) {
|
|
860
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
861
|
+
console.log(green(`\nā
Configurazione salvata!`));
|
|
862
|
+
console.log(yellow("ā ļø Non condividere config.json - contiene il tuo token.\n"));
|
|
863
|
+
}
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
const verifyToken = async () => {
|
|
867
|
+
const apiToken = await promptInput(cyan("š Token da verificare: "));
|
|
868
|
+
const spin = spinner("Verifica in corso...").start();
|
|
869
|
+
|
|
870
|
+
try {
|
|
871
|
+
const res = await fetch(`${API_BASE}/tokens/verify`, {
|
|
872
|
+
method: "POST",
|
|
873
|
+
headers: { "Content-Type": "application/json" },
|
|
874
|
+
body: JSON.stringify({ token: apiToken })
|
|
875
|
+
});
|
|
876
|
+
const data = await res.json();
|
|
877
|
+
|
|
878
|
+
if (data.valid) {
|
|
879
|
+
spin.succeed("Token valido!");
|
|
880
|
+
console.log(` ${gray('Etichetta:')} ${data.label}`);
|
|
881
|
+
console.log(` ${gray('Creato:')} ${new Date(data.createdAt).toLocaleString("it-IT")}\n`);
|
|
882
|
+
} else {
|
|
883
|
+
spin.fail("Token non valido");
|
|
884
|
+
}
|
|
885
|
+
} catch (err) {
|
|
886
|
+
spin.fail("Errore nella verifica");
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
const showCurrentStatus = async () => {
|
|
891
|
+
const cliToken = await getCliToken();
|
|
892
|
+
const spin = spinner("Caricamento stato...").start();
|
|
893
|
+
|
|
894
|
+
const remote = await fetchRemoteConfig(cliToken);
|
|
895
|
+
if (!remote) {
|
|
896
|
+
spin.fail("Impossibile caricare la configurazione");
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
const currentIP = await getCurrentIP(remote.ipService).catch(() => null);
|
|
901
|
+
const lastIP = loadLastIP();
|
|
902
|
+
|
|
903
|
+
spin.stop();
|
|
904
|
+
|
|
905
|
+
console.log(`\nš ${chalk.bold('Stato Attuale')}\n`);
|
|
906
|
+
|
|
907
|
+
const table = new Table({ style: { border: ['gray'] } });
|
|
908
|
+
table.push(
|
|
909
|
+
[gray('Dominio'), cyan.bold(remote.domain)],
|
|
910
|
+
[gray('TTL'), `${remote.ttl}s`],
|
|
911
|
+
[gray('IP Attuale'), green.bold(currentIP || 'N/D')],
|
|
912
|
+
[gray('Ultimo IP'), lastIP || gray('N/D')],
|
|
913
|
+
[gray('IPv6'), remote.useIPv6 ? green('Attivo') : gray('Disattivo')]
|
|
914
|
+
);
|
|
915
|
+
|
|
916
|
+
console.log(table.toString());
|
|
917
|
+
console.log();
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const editConfig = async () => {
|
|
921
|
+
const apiToken = await getAuthToken();
|
|
922
|
+
const remote = await fetchRemoteConfig(apiToken);
|
|
923
|
+
if (!remote) {
|
|
924
|
+
console.log(red("Impossibile caricare la configurazione."));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const answers = await inquirer.prompt([
|
|
929
|
+
{ type: "input", name: "ttl", message: "ā±ļø TTL (secondi):", default: String(remote.ttl) },
|
|
930
|
+
{ type: "input", name: "ipService", message: "š Servizio IP:", default: remote.ipService },
|
|
931
|
+
{ type: "confirm", name: "useIPv6", message: "6ļøā£ Usare IPv6?", default: remote.useIPv6 }
|
|
932
|
+
]);
|
|
933
|
+
|
|
934
|
+
const spin = spinner("Salvataggio...").start();
|
|
935
|
+
const res = await fetch(`${API_BASE}/cli-config/${remote.id}`, {
|
|
936
|
+
method: "PATCH",
|
|
937
|
+
headers: {
|
|
938
|
+
"Content-Type": "application/json",
|
|
939
|
+
Authorization: `Bearer ${apiToken}`
|
|
940
|
+
},
|
|
941
|
+
body: JSON.stringify({
|
|
942
|
+
ttl: parseInt(answers.ttl),
|
|
943
|
+
ipService: answers.ipService,
|
|
944
|
+
useIPv6: answers.useIPv6
|
|
945
|
+
})
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
if (res.ok) {
|
|
949
|
+
spin.succeed("Configurazione aggiornata!");
|
|
950
|
+
config = { ...remote, ...answers, apiToken };
|
|
951
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
952
|
+
} else {
|
|
953
|
+
const data = await res.json();
|
|
954
|
+
spin.fail("Errore: " + (data.error || data.message));
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const runUpdate = async () => {
|
|
959
|
+
let apiToken = config.apiToken;
|
|
960
|
+
if (!apiToken) {
|
|
961
|
+
apiToken = await promptInput(cyan("š Token API: "));
|
|
962
|
+
const remoteConfig = await fetchRemoteConfig(apiToken);
|
|
963
|
+
if (!remoteConfig) {
|
|
964
|
+
console.log(red("Configurazione non trovata."));
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
config = { ...remoteConfig, apiToken };
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const spin = spinner("Rilevamento IP...").start();
|
|
971
|
+
const currentIP = await getCurrentIP(config.ipService).catch(() => null);
|
|
972
|
+
const currentIPv6 = config.useIPv6 ? await getCurrentIPv6(config.ipv6Service).catch(() => null) : null;
|
|
973
|
+
|
|
974
|
+
if (!currentIP && !currentIPv6) {
|
|
975
|
+
spin.fail("Nessun IP rilevato");
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
spin.text = `IP rilevato: ${currentIP}`;
|
|
980
|
+
|
|
981
|
+
const lastIP = loadLastIP();
|
|
982
|
+
if (!forceUpdate && lastIP === currentIP) {
|
|
983
|
+
spin.succeed(`IP invariato (${currentIP})`);
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
spin.text = `Aggiornamento DNS per ${config.domain}...`;
|
|
988
|
+
|
|
989
|
+
const body = {
|
|
990
|
+
name: config.domain,
|
|
991
|
+
ip: currentIP,
|
|
992
|
+
ttl: config.ttl,
|
|
993
|
+
};
|
|
994
|
+
if (currentIPv6) body.ipv6 = currentIPv6;
|
|
995
|
+
|
|
996
|
+
const res = await fetch(`${API_BASE}/update-dns`, {
|
|
997
|
+
method: "POST",
|
|
998
|
+
headers: {
|
|
999
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
1000
|
+
"Content-Type": "application/json",
|
|
1001
|
+
},
|
|
1002
|
+
body: JSON.stringify(body),
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
const data = await res.json();
|
|
1006
|
+
if (res.ok && data.results) {
|
|
1007
|
+
saveCurrentIP(currentIP);
|
|
1008
|
+
spin.succeed(`DNS aggiornato! ${config.domain} ā ${currentIP}`);
|
|
1009
|
+
if (showJson) console.log(JSON.stringify(data.results[0], null, 2));
|
|
1010
|
+
} else {
|
|
1011
|
+
spin.fail(`Errore: ${data.error || data.details}`);
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// ==================== INTERACTIVE MODE ====================
|
|
1016
|
+
|
|
1017
|
+
const interactiveMode = async () => {
|
|
1018
|
+
console.log(gray(" Premi Ctrl+C per uscire\n"));
|
|
1019
|
+
|
|
1020
|
+
while (true) {
|
|
1021
|
+
const { action } = await inquirer.prompt([{
|
|
1022
|
+
type: "list",
|
|
1023
|
+
name: "action",
|
|
1024
|
+
message: orange("Cosa vuoi fare?"),
|
|
1025
|
+
pageSize: 15,
|
|
1026
|
+
choices: [
|
|
1027
|
+
{ name: `${orange('š')} Dashboard`, value: "dashboard" },
|
|
1028
|
+
new inquirer.Separator(gray('āāā Domini āāā')),
|
|
1029
|
+
{ name: `${cyan('š')} Lista domini`, value: "domains" },
|
|
1030
|
+
{ name: `${green('ā')} Aggiungi dominio`, value: "add-domain" },
|
|
1031
|
+
{ name: `${red('šļø ')} Elimina dominio`, value: "delete-domain" },
|
|
1032
|
+
{ name: `${blue('š')} Test DNS`, value: "test-dns" },
|
|
1033
|
+
new inquirer.Separator(gray('āāā Token āāā')),
|
|
1034
|
+
{ name: `${purple('š')} Lista token`, value: "tokens" },
|
|
1035
|
+
{ name: `${yellow('š')} Toggle token`, value: "toggle-token" },
|
|
1036
|
+
new inquirer.Separator(gray('āāā Altro āāā')),
|
|
1037
|
+
{ name: `${cyan('š')} Statistiche`, value: "stats" },
|
|
1038
|
+
{ name: `${gray('š')} Log attivitĆ `, value: "logs" },
|
|
1039
|
+
{ name: `${purple('š')} Webhooks`, value: "webhooks" },
|
|
1040
|
+
{ name: `${blue('š')} API Keys`, value: "api-keys" },
|
|
1041
|
+
new inquirer.Separator(gray('āāā Config āāā')),
|
|
1042
|
+
{ name: `${gray('š')} Stato attuale`, value: "status" },
|
|
1043
|
+
{ name: `${green('š')} Aggiorna DNS`, value: "update" },
|
|
1044
|
+
{ name: `${yellow('āļø ')} Configurazione`, value: "config" },
|
|
1045
|
+
{ name: `${blue('š§')} Setup`, value: "setup" },
|
|
1046
|
+
new inquirer.Separator(),
|
|
1047
|
+
{ name: red("ā Esci"), value: "exit" }
|
|
1048
|
+
]
|
|
1049
|
+
}]);
|
|
1050
|
+
|
|
1051
|
+
console.log();
|
|
1052
|
+
|
|
1053
|
+
switch (action) {
|
|
1054
|
+
case "dashboard": await showDashboardCommand(); break;
|
|
1055
|
+
case "domains": await showDomainsList(); break;
|
|
1056
|
+
case "add-domain": await addDomain(); break;
|
|
1057
|
+
case "delete-domain": await deleteDomain(); break;
|
|
1058
|
+
case "test-dns": await testDnsResolution(); break;
|
|
1059
|
+
case "tokens": await showTokensList(); break;
|
|
1060
|
+
case "toggle-token":
|
|
1061
|
+
const tokens = await fetchTokens();
|
|
1062
|
+
if (tokens.length === 0) {
|
|
1063
|
+
console.log(yellow("Nessun token disponibile."));
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
const { tokenId } = await inquirer.prompt([{
|
|
1067
|
+
type: "list",
|
|
1068
|
+
name: "tokenId",
|
|
1069
|
+
message: "Seleziona token:",
|
|
1070
|
+
choices: tokens.map(t => ({
|
|
1071
|
+
name: `${t.active ? green('ā') : red('ā')} ${t.label} ${gray('ā')} ${t.domain?.name || 'N/D'}`,
|
|
1072
|
+
value: t.id
|
|
1073
|
+
}))
|
|
1074
|
+
}]);
|
|
1075
|
+
await updateTokenState(tokenId, null);
|
|
1076
|
+
break;
|
|
1077
|
+
case "stats": await showStatsCommand(); break;
|
|
1078
|
+
case "logs": await showLogsCommand(); break;
|
|
1079
|
+
case "webhooks": await showWebhooksList(); break;
|
|
1080
|
+
case "api-keys": await showApiKeysList(); break;
|
|
1081
|
+
case "status": await showCurrentStatus(); break;
|
|
1082
|
+
case "update": await runUpdate(); break;
|
|
1083
|
+
case "config": await editConfig(); break;
|
|
1084
|
+
case "setup": await setup(); break;
|
|
1085
|
+
case "exit":
|
|
1086
|
+
console.log(gray("Arrivederci! š\n"));
|
|
1087
|
+
process.exit(0);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
console.log();
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
|
|
1094
|
+
// ==================== MAIN ====================
|
|
1095
|
+
|
|
1096
|
+
const main = async () => {
|
|
1097
|
+
try {
|
|
1098
|
+
if (enableTokenId) await updateTokenState(enableTokenId, true);
|
|
1099
|
+
else if (disableTokenId) await updateTokenState(disableTokenId, false);
|
|
1100
|
+
else if (toggleTokenId) await updateTokenState(toggleTokenId, null);
|
|
1101
|
+
else if (showDashboard) await showDashboardCommand();
|
|
1102
|
+
else if (listDomains) await showDomainsList();
|
|
1103
|
+
else if (addDomainArg) await addDomain(addDomainArg);
|
|
1104
|
+
else if (deleteDomainArg) await deleteDomain(deleteDomainArg);
|
|
1105
|
+
else if (testDns) await testDnsResolution(testDns);
|
|
1106
|
+
else if (listTokens) await showTokensList();
|
|
1107
|
+
else if (showStats) await showStatsCommand();
|
|
1108
|
+
else if (showLogs) await showLogsCommand();
|
|
1109
|
+
else if (listWebhooks) await showWebhooksList();
|
|
1110
|
+
else if (listApiKeys) await showApiKeysList();
|
|
1111
|
+
else if (runSetup) await setup();
|
|
1112
|
+
else if (runVerify) await verifyToken();
|
|
1113
|
+
else if (showStatus) await showCurrentStatus();
|
|
1114
|
+
else if (runConfigEdit) await editConfig();
|
|
1115
|
+
else if (runInteractive) await interactiveMode();
|
|
1116
|
+
else await runUpdate();
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
if (err.message !== 'User force closed the prompt') {
|
|
1119
|
+
console.error(red("\nā Errore:"), err.message);
|
|
1120
|
+
}
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "apertodns",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ApertoDNS CLI - Dynamic DNS management from your terminal. Manage domains, tokens, and DNS updates.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"apertodns": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node index.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"ddns",
|
|
15
|
+
"cli",
|
|
16
|
+
"dns",
|
|
17
|
+
"apertodns",
|
|
18
|
+
"dynamic-dns",
|
|
19
|
+
"ipv4",
|
|
20
|
+
"ipv6",
|
|
21
|
+
"domain",
|
|
22
|
+
"terminal"
|
|
23
|
+
],
|
|
24
|
+
"author": "Aperto Network <info@apertodns.com>",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/apertonetwork/apertodns.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://apertodns.com",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/apertonetwork/apertodns/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18.0.0"
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"index.js",
|
|
39
|
+
"utils.js",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"cli-spinners": "^3.3.0",
|
|
45
|
+
"cli-table3": "^0.6.5",
|
|
46
|
+
"figlet": "^1.6.0",
|
|
47
|
+
"inquirer": "^9.3.8",
|
|
48
|
+
"node-fetch": "^3.3.2",
|
|
49
|
+
"ora": "^9.0.0"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/utils.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fetch from "node-fetch";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const dataDir = path.resolve(__dirname, ".data");
|
|
8
|
+
const ipPath = path.join(dataDir, "last_ip_ironDNS.txt");
|
|
9
|
+
|
|
10
|
+
if (!fs.existsSync(dataDir)) {
|
|
11
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const log = (msg) => {
|
|
15
|
+
const time = new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
16
|
+
console.log(`[${time}] ${msg}`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getCurrentIP = async (ipService) => {
|
|
20
|
+
const res = await fetch(ipService);
|
|
21
|
+
return res.text();
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const loadLastIP = () => {
|
|
25
|
+
return fs.existsSync(ipPath) ? fs.readFileSync(ipPath, "utf-8") : null;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const saveCurrentIP = (ip) => {
|
|
29
|
+
fs.writeFileSync(ipPath, ip);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const getCurrentIPv6 = async (ipService6) => {
|
|
33
|
+
const res = await fetch(ipService6);
|
|
34
|
+
return res.text();
|
|
35
|
+
};
|