jskelet 0.2.2 → 0.2.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/CHANGELOG.md +375 -315
- package/docs/03-routing.md +4 -0
- package/docs/06-cache.md +154 -10
- package/docs/07-yapilandirma.md +63 -2
- package/docs/en/03-routing.md +4 -0
- package/docs/en/06-caching.md +154 -11
- package/docs/en/07-configuration.md +65 -2
- package/package.json +1 -1
- package/src/client/cache-panel/i18n.js +670 -0
- package/src/client/cache-panel/login.html +23 -12
- package/src/client/cache-panel/panel.css +314 -6
- package/src/client/cache-panel/panel.html +205 -30
- package/src/client/cache-panel/panel.js +605 -29
- package/src/config/defaults.js +31 -0
- package/src/config/index.js +87 -2
- package/src/index.js +16 -1
- package/src/log.mjs +25 -0
- package/src/server/cache-panel.js +294 -40
- package/src/server/cloudflare.js +595 -0
- package/src/server/redis.js +108 -0
- package/src/server/render.js +74 -2
- package/src/version.mjs +9 -0
|
@@ -26,10 +26,18 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import crypto from "node:crypto";
|
|
28
28
|
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
29
30
|
import path from "node:path";
|
|
30
31
|
import process from "node:process";
|
|
31
32
|
import express from "express";
|
|
33
|
+
import * as log from "../log.mjs";
|
|
32
34
|
import { FRAMEWORK_ROOT, getConfig } from "../config/index.js";
|
|
35
|
+
import {
|
|
36
|
+
FRAMEWORK_HOMEPAGE,
|
|
37
|
+
FRAMEWORK_LICENSE,
|
|
38
|
+
FRAMEWORK_NODE_RANGE,
|
|
39
|
+
FRAMEWORK_VERSION,
|
|
40
|
+
} from "../version.mjs";
|
|
33
41
|
import { safeEqual } from "../http/cookies.js";
|
|
34
42
|
import {
|
|
35
43
|
clearHtmlCache,
|
|
@@ -44,8 +52,25 @@ import {
|
|
|
44
52
|
getDataCacheEntries,
|
|
45
53
|
getDataCacheSize,
|
|
46
54
|
} from "./data-cache.js";
|
|
47
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
getRedisDetails,
|
|
57
|
+
getRedisStatus,
|
|
58
|
+
inspectRedis,
|
|
59
|
+
redisDropMatching,
|
|
60
|
+
} from "./redis.js";
|
|
48
61
|
import { getUpstreamLimiterStatus } from "./upstream-limiter.js";
|
|
62
|
+
import {
|
|
63
|
+
clearCloudflareCacheReserve,
|
|
64
|
+
cloudflareConfigured,
|
|
65
|
+
fetchCacheAnalytics,
|
|
66
|
+
fetchCloudflareOverview,
|
|
67
|
+
fetchPathEdges,
|
|
68
|
+
getCloudflareStatus,
|
|
69
|
+
purgeCloudflare,
|
|
70
|
+
setCloudflareFeature,
|
|
71
|
+
setCloudflareSetting,
|
|
72
|
+
toCloudflareUrls,
|
|
73
|
+
} from "./cloudflare.js";
|
|
49
74
|
import { prewarm, prewarmProgress } from "./prewarm.js";
|
|
50
75
|
|
|
51
76
|
/** Panel dosyaları framework paketinden servis edilir, uygulamadan değil. */
|
|
@@ -199,9 +224,9 @@ function notFound(res) {
|
|
|
199
224
|
* göndermenin faydası yok.
|
|
200
225
|
*
|
|
201
226
|
* @param {string} query Anahtar filtresi (boş → filtre yok).
|
|
202
|
-
* @returns {object}
|
|
227
|
+
* @returns {Promise<object>}
|
|
203
228
|
*/
|
|
204
|
-
function snapshot(query) {
|
|
229
|
+
async function snapshot(query) {
|
|
205
230
|
const term = query.trim().toLowerCase();
|
|
206
231
|
/** @param {{ key: string }} entry */
|
|
207
232
|
const matches = (entry) => !term || entry.key.toLowerCase().includes(term);
|
|
@@ -216,6 +241,13 @@ function snapshot(query) {
|
|
|
216
241
|
return {
|
|
217
242
|
boot: BOOT_ID,
|
|
218
243
|
generatedAt: Date.now(),
|
|
244
|
+
release: {
|
|
245
|
+
version: FRAMEWORK_VERSION,
|
|
246
|
+
license: FRAMEWORK_LICENSE,
|
|
247
|
+
node: FRAMEWORK_NODE_RANGE,
|
|
248
|
+
homepage: FRAMEWORK_HOMEPAGE,
|
|
249
|
+
},
|
|
250
|
+
host: await hostStatus(),
|
|
219
251
|
process: {
|
|
220
252
|
pid: process.pid,
|
|
221
253
|
node: process.version,
|
|
@@ -231,7 +263,14 @@ function snapshot(query) {
|
|
|
231
263
|
matched: htmlMatched.length,
|
|
232
264
|
entries: htmlMatched
|
|
233
265
|
.sort((a, b) => b.bytes - a.bytes)
|
|
234
|
-
.slice(0, MAX_LISTED)
|
|
266
|
+
.slice(0, MAX_LISTED)
|
|
267
|
+
// Anahtar `yol?query`; query boşken sonda yalnız bir `?` kalıyor.
|
|
268
|
+
// Silme işlemleri gerçek anahtarı kullanmaya devam eder, panel
|
|
269
|
+
// ziyaret edilebilir yolu gösterir.
|
|
270
|
+
.map((entry) => ({
|
|
271
|
+
...entry,
|
|
272
|
+
url: entry.key.endsWith("?") ? entry.key.slice(0, -1) : entry.key,
|
|
273
|
+
})),
|
|
235
274
|
},
|
|
236
275
|
data: {
|
|
237
276
|
size: getDataCacheSize(),
|
|
@@ -242,16 +281,68 @@ function snapshot(query) {
|
|
|
242
281
|
.sort((a, b) => a.expiresIn - b.expiresIn)
|
|
243
282
|
.slice(0, MAX_LISTED),
|
|
244
283
|
},
|
|
245
|
-
redis: getRedisStatus(),
|
|
284
|
+
redis: { ...getRedisStatus(), ...getRedisDetails() },
|
|
285
|
+
// Cloudflare yalnızca kurulum özeti: zone bilgisi ve analitik ağa çıkıyor
|
|
286
|
+
// ve döküm birkaç saniyede bir yenileniyor. Ayrıntı ayrı uçta.
|
|
287
|
+
cloudflare: getCloudflareStatus(),
|
|
246
288
|
upstream: getUpstreamLimiterStatus(),
|
|
247
289
|
prewarm: { ...prewarmProgress },
|
|
248
290
|
};
|
|
249
291
|
}
|
|
250
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Makinenin RAM ve disk durumu.
|
|
295
|
+
*
|
|
296
|
+
* Paylaşımlı kademe kapalıyken önbelleğin tamamı bu sürecin belleğinde
|
|
297
|
+
* yaşıyor ve `maxEntries` ile RAM arasındaki ilişki panelde görünmediği sürece
|
|
298
|
+
* ayarlanamıyor: "500 girdi çok mu" sorusunun cevabı makinede ne kadar boş
|
|
299
|
+
* bellek kaldığına bağlı. Disk, HTML önbelleğinin kendisi için değil, ısıtma
|
|
300
|
+
* ve build çıktısının yaşadığı yer için: dolu bir diskte `jskelet build`
|
|
301
|
+
* sessizce hatalı manifest bırakıyor.
|
|
302
|
+
*
|
|
303
|
+
* `statfs` desteklenmeyen bir platform ya da erişilemeyen bir yol sadece
|
|
304
|
+
* `null` disk demek; panel yine açılır.
|
|
305
|
+
*
|
|
306
|
+
* @returns {Promise<object>}
|
|
307
|
+
*/
|
|
308
|
+
async function hostStatus() {
|
|
309
|
+
const total = os.totalmem();
|
|
310
|
+
const free = os.freemem();
|
|
311
|
+
|
|
312
|
+
/** @type {{ path: string, total: number, free: number } | null} */
|
|
313
|
+
let disk = null;
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
const root = getConfig().root;
|
|
317
|
+
const stats = await fs.promises.statfs(root);
|
|
318
|
+
disk = {
|
|
319
|
+
path: root,
|
|
320
|
+
total: stats.blocks * stats.bsize,
|
|
321
|
+
// `bavail`, `bfree` değil: ayrılmış blokları boş göstermek yanıltıcı.
|
|
322
|
+
free: stats.bavail * stats.bsize,
|
|
323
|
+
};
|
|
324
|
+
} catch {
|
|
325
|
+
disk = null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
platform: `${os.platform()} ${os.arch()}`,
|
|
330
|
+
cpus: os.cpus().length,
|
|
331
|
+
// Windows'ta `loadavg()` her zaman sıfır döner; panel bunu gizler.
|
|
332
|
+
load: os.loadavg()[0],
|
|
333
|
+
memory: { total, free, used: total - free },
|
|
334
|
+
disk,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
251
338
|
/* --------------------------------------------------------------- aksiyonlar */
|
|
252
339
|
|
|
253
340
|
/**
|
|
254
|
-
* Panelden gelen işlemi uygular ve
|
|
341
|
+
* Panelden gelen işlemi uygular ve sonucu **metin değil kod** olarak döner.
|
|
342
|
+
*
|
|
343
|
+
* Cümleyi panel kuruyor (`i18n.js`): panel iki dilde konuşuyor, sunucunun
|
|
344
|
+
* arayüz dilini bilmesi için bir sebep yok. `params` yer tutucuları besler,
|
|
345
|
+
* `parts` ise eksik kalabilen parçalı cevaplar için.
|
|
255
346
|
*
|
|
256
347
|
* Her işlem yerel kademeyi düşürüp paylaşımlı kademeye de yayılıyor (bkz.
|
|
257
348
|
* `html-cache.js`, `data-cache.js`): tek node'un önbelleğini boşaltmak,
|
|
@@ -259,7 +350,9 @@ function snapshot(query) {
|
|
|
259
350
|
*
|
|
260
351
|
* @param {Record<string, any>} body
|
|
261
352
|
* @param {import('express').Request} req
|
|
262
|
-
* @returns {Promise<{ ok: boolean,
|
|
353
|
+
* @returns {Promise<{ ok: boolean, code?: string,
|
|
354
|
+
* params?: Record<string, string | number>,
|
|
355
|
+
* parts?: ({ code: string, params?: Record<string, string | number> } | null)[] }>}
|
|
263
356
|
*/
|
|
264
357
|
async function runAction(body, req) {
|
|
265
358
|
const type = String(body?.type ?? "");
|
|
@@ -268,51 +361,63 @@ async function runAction(body, req) {
|
|
|
268
361
|
case "html:clear": {
|
|
269
362
|
const size = getHtmlCacheSize();
|
|
270
363
|
clearHtmlCache();
|
|
271
|
-
return { ok: true,
|
|
364
|
+
return { ok: true, code: "html.cleared", params: { count: size } };
|
|
272
365
|
}
|
|
273
366
|
|
|
274
367
|
case "data:clear": {
|
|
275
368
|
const prefix = typeof body.prefix === "string" && body.prefix ? body.prefix : undefined;
|
|
276
369
|
const removed = clearDataCache(prefix);
|
|
277
|
-
return
|
|
278
|
-
ok: true,
|
|
279
|
-
|
|
280
|
-
? `${removed} data entries dropped under \`${prefix}\``
|
|
281
|
-
: `Data cache cleared (${removed} entries)`,
|
|
282
|
-
};
|
|
370
|
+
return prefix
|
|
371
|
+
? { ok: true, code: "data.clearedPrefix", params: { count: removed, prefix } }
|
|
372
|
+
: { ok: true, code: "data.cleared", params: { count: removed } };
|
|
283
373
|
}
|
|
284
374
|
|
|
285
375
|
case "html:invalidate": {
|
|
286
376
|
const target = String(body.target ?? "");
|
|
287
377
|
if (!target.startsWith("/")) {
|
|
288
|
-
return { ok: false,
|
|
378
|
+
return { ok: false, code: "target.invalid" };
|
|
289
379
|
}
|
|
290
380
|
|
|
291
381
|
const hard = body.hard === true;
|
|
292
382
|
const count = invalidateHtmlCache(target, { hard });
|
|
293
383
|
return {
|
|
294
384
|
ok: true,
|
|
295
|
-
|
|
385
|
+
code: hard ? "html.dropped" : "html.marked",
|
|
386
|
+
params: { count, target },
|
|
296
387
|
};
|
|
297
388
|
}
|
|
298
389
|
|
|
299
390
|
case "html:drop": {
|
|
300
391
|
const key = String(body.key ?? "");
|
|
301
|
-
if (!key) return { ok: false,
|
|
392
|
+
if (!key) return { ok: false, code: "key.missing" };
|
|
302
393
|
const existed = dropHtmlCacheKey(key);
|
|
303
|
-
return {
|
|
304
|
-
ok: true,
|
|
305
|
-
message: existed ? `Dropped \`${key}\`` : `\`${key}\` was not cached`,
|
|
306
|
-
};
|
|
394
|
+
return { ok: true, code: existed ? "entry.dropped" : "entry.absent", params: { key } };
|
|
307
395
|
}
|
|
308
396
|
|
|
309
397
|
case "data:drop": {
|
|
310
398
|
const key = String(body.key ?? "");
|
|
311
|
-
if (!key) return { ok: false,
|
|
399
|
+
if (!key) return { ok: false, code: "key.missing" };
|
|
312
400
|
const existed = dropDataCacheKey(key);
|
|
401
|
+
return { ok: true, code: existed ? "entry.dropped" : "entry.absent", params: { key } };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
case "redis:inspect": {
|
|
405
|
+
const result = await inspectRedis();
|
|
406
|
+
if (!result.ok) return { ok: false, code: "redis.unreachable" };
|
|
407
|
+
|
|
408
|
+
// Sayım tek cümle değil parçalar hâlinde dönüyor: `DBSIZE` ve
|
|
409
|
+
// `used_memory` komut kısıtlı bir Redis'te okunamıyor ve eksik parçayı
|
|
410
|
+
// cümleden düşürme kararı panelin.
|
|
313
411
|
return {
|
|
314
412
|
ok: true,
|
|
315
|
-
|
|
413
|
+
parts: [
|
|
414
|
+
{ code: "redis.htmlKeys", params: { count: result.html } },
|
|
415
|
+
{ code: "redis.dataKeys", params: { count: result.data } },
|
|
416
|
+
result.totalKeys !== null
|
|
417
|
+
? { code: "redis.dbKeys", params: { count: result.totalKeys } }
|
|
418
|
+
: null,
|
|
419
|
+
result.usedMemory ? { code: "redis.memory", params: { value: result.usedMemory } } : null,
|
|
420
|
+
].filter(Boolean),
|
|
316
421
|
};
|
|
317
422
|
}
|
|
318
423
|
|
|
@@ -320,16 +425,96 @@ async function runAction(body, req) {
|
|
|
320
425
|
const kind = body.kind === "data" ? "data" : "html";
|
|
321
426
|
const status = getRedisStatus();
|
|
322
427
|
if (!status.connected) {
|
|
323
|
-
return { ok: false,
|
|
428
|
+
return { ok: false, code: "redis.notConnected" };
|
|
324
429
|
}
|
|
325
430
|
|
|
326
431
|
const dropped = await redisDropMatching(kind);
|
|
327
|
-
return {
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
code: kind === "data" ? "redis.droppedData" : "redis.droppedHtml",
|
|
435
|
+
params: { count: dropped },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* ------------------------------------------------------- cloudflare */
|
|
440
|
+
|
|
441
|
+
case "cf:purge-everything": {
|
|
442
|
+
const result = await purgeCloudflare({ everything: true });
|
|
443
|
+
return result.ok
|
|
444
|
+
? { ok: true, code: "cf.purgedEverything" }
|
|
445
|
+
: cloudflareFailure(result.error);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
case "cf:purge-urls": {
|
|
449
|
+
// Panelin elindeki HTML anahtarları yol; Cloudflare tam URL istiyor.
|
|
450
|
+
// Origin, isteğin geldiği host'tan türetilir ki tek zone'lu kurulumda
|
|
451
|
+
// ayrıca ayar gerekmesin.
|
|
452
|
+
const paths = Array.isArray(body.paths) ? body.paths.map(String) : [];
|
|
453
|
+
if (!paths.length) return { ok: false, code: "cf.noPaths" };
|
|
454
|
+
|
|
455
|
+
const urls = toCloudflareUrls(paths, originOf(req));
|
|
456
|
+
if (!urls.length) return { ok: false, code: "cf.noHostname" };
|
|
457
|
+
|
|
458
|
+
const result = await purgeCloudflare({ files: urls });
|
|
459
|
+
return result.ok
|
|
460
|
+
? {
|
|
461
|
+
ok: true,
|
|
462
|
+
code: "cf.purgedUrls",
|
|
463
|
+
params: { count: result.purged, batches: result.batches },
|
|
464
|
+
}
|
|
465
|
+
: cloudflareFailure(result.error);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
case "cf:purge-keys": {
|
|
469
|
+
/** @type {"prefixes" | "hosts" | "tags"} */
|
|
470
|
+
const kind =
|
|
471
|
+
body.kind === "hosts" ? "hosts" : body.kind === "tags" ? "tags" : "prefixes";
|
|
472
|
+
|
|
473
|
+
const values = String(body.values ?? "")
|
|
474
|
+
.split(/[\s,]+/)
|
|
475
|
+
.filter(Boolean);
|
|
476
|
+
|
|
477
|
+
if (!values.length) return { ok: false, code: "cf.nothingToPurge" };
|
|
478
|
+
|
|
479
|
+
const result = await purgeCloudflare({ [kind]: values });
|
|
480
|
+
return result.ok
|
|
481
|
+
? { ok: true, code: "cf.purgedKeys", params: { count: result.purged, kind } }
|
|
482
|
+
: cloudflareFailure(result.error);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
case "cf:setting": {
|
|
486
|
+
const result = await setCloudflareSetting(body.id, body.value);
|
|
487
|
+
return result.ok
|
|
488
|
+
? {
|
|
489
|
+
ok: true,
|
|
490
|
+
code: "cf.settingChanged",
|
|
491
|
+
params: { id: String(body.id), value: String(result.value) },
|
|
492
|
+
}
|
|
493
|
+
: cloudflareFailure(result.error);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
case "cf:feature": {
|
|
497
|
+
const value = body.value === "on" ? "on" : "off";
|
|
498
|
+
const result = await setCloudflareFeature(body.feature, value);
|
|
499
|
+
return result.ok
|
|
500
|
+
? {
|
|
501
|
+
ok: true,
|
|
502
|
+
code: "cf.featureChanged",
|
|
503
|
+
params: { feature: String(body.feature), value },
|
|
504
|
+
}
|
|
505
|
+
: cloudflareFailure(result.error);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
case "cf:clear-reserve": {
|
|
509
|
+
const result = await clearCloudflareCacheReserve();
|
|
510
|
+
return result.ok
|
|
511
|
+
? { ok: true, code: "cf.reserveClearing" }
|
|
512
|
+
: cloudflareFailure(result.error);
|
|
328
513
|
}
|
|
329
514
|
|
|
330
515
|
case "prewarm": {
|
|
331
516
|
if (prewarmProgress.active) {
|
|
332
|
-
return { ok: false,
|
|
517
|
+
return { ok: false, code: "prewarm.busy" };
|
|
333
518
|
}
|
|
334
519
|
|
|
335
520
|
const requested = Array.isArray(body.paths) ? body.paths : [];
|
|
@@ -347,17 +532,27 @@ async function runAction(body, req) {
|
|
|
347
532
|
console.error("[cache-panel] prewarm failed", error);
|
|
348
533
|
});
|
|
349
534
|
|
|
350
|
-
return
|
|
351
|
-
ok: true,
|
|
352
|
-
|
|
353
|
-
};
|
|
535
|
+
return paths.length
|
|
536
|
+
? { ok: true, code: "prewarm.paths", params: { count: paths.length } }
|
|
537
|
+
: { ok: true, code: "prewarm.all" };
|
|
354
538
|
}
|
|
355
539
|
|
|
356
540
|
default:
|
|
357
|
-
return { ok: false,
|
|
541
|
+
return { ok: false, code: "action.unknown", params: { type } };
|
|
358
542
|
}
|
|
359
543
|
}
|
|
360
544
|
|
|
545
|
+
/**
|
|
546
|
+
* Cloudflare hatası tek bir kodla döner: hata metni Cloudflare'den geliyor ve
|
|
547
|
+
* çevrilemez, ama etrafındaki cümle çevrilebilir.
|
|
548
|
+
*
|
|
549
|
+
* @param {string | undefined} error
|
|
550
|
+
* @returns {{ ok: false, code: string, params: { error: string } }}
|
|
551
|
+
*/
|
|
552
|
+
function cloudflareFailure(error) {
|
|
553
|
+
return { ok: false, code: "cf.failed", params: { error: error ?? "unknown error" } };
|
|
554
|
+
}
|
|
555
|
+
|
|
361
556
|
/* ------------------------------------------------------------------ router */
|
|
362
557
|
|
|
363
558
|
/**
|
|
@@ -396,13 +591,24 @@ function router() {
|
|
|
396
591
|
sendFile(res, authenticated(req) ? "panel.html" : "login.html");
|
|
397
592
|
});
|
|
398
593
|
|
|
399
|
-
// Stil giriş sayfasında da gerekiyor, yani oturumdan önce servis
|
|
400
|
-
//
|
|
594
|
+
// Stil ve logo giriş sayfasında da gerekiyor, yani oturumdan önce servis
|
|
595
|
+
// edilir. İkisinde de durum bilgisi yok.
|
|
401
596
|
api.get("/panel.css", (req, res) => {
|
|
402
597
|
res.type("text/css");
|
|
403
598
|
sendFile(res, "panel.css");
|
|
404
599
|
});
|
|
405
600
|
|
|
601
|
+
// Sözlük giriş sayfasında da gerekiyor: dil seçimi oturumdan önce yapılıyor.
|
|
602
|
+
api.get("/i18n.js", (req, res) => {
|
|
603
|
+
res.type("application/javascript");
|
|
604
|
+
sendFile(res, "i18n.js");
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
api.get("/logo.png", (req, res) => {
|
|
608
|
+
res.type("image/png");
|
|
609
|
+
fs.createReadStream(path.join(FRAMEWORK_ROOT, "src", "logo.png")).pipe(res);
|
|
610
|
+
});
|
|
611
|
+
|
|
406
612
|
api.post("/login", express.json({ limit: "4kb" }), (req, res) => {
|
|
407
613
|
const password = typeof req.body?.password === "string" ? req.body.password : "";
|
|
408
614
|
|
|
@@ -441,7 +647,15 @@ function router() {
|
|
|
441
647
|
api.use((req, res, next) => {
|
|
442
648
|
if (authenticated(req)) return next();
|
|
443
649
|
|
|
444
|
-
|
|
650
|
+
// Yetkisiz **okuma** sayaca yazılmaz. Süreç yeniden başladığında eski
|
|
651
|
+
// oturumlar ölüyor ve açık kalmış bir panel sekmesi yoklamaya devam
|
|
652
|
+
// ediyor: sayarsak operatör kendi IP'sini üç saniyede yasaklıyor. Okuma
|
|
653
|
+
// zaten `404` dönüyor, yani saymamanın bir bedeli yok — korunması gereken
|
|
654
|
+
// şey şifre denemesi ve durumu değiştiren istek.
|
|
655
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
656
|
+
noteFailure(req, `unauthenticated ${req.method} ${req.path}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
445
659
|
notFound(res);
|
|
446
660
|
});
|
|
447
661
|
|
|
@@ -457,9 +671,29 @@ function router() {
|
|
|
457
671
|
sendFile(res, "panel.js");
|
|
458
672
|
});
|
|
459
673
|
|
|
460
|
-
api.get("/data", (req, res) => {
|
|
674
|
+
api.get("/data", async (req, res) => {
|
|
461
675
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
462
|
-
res.json(snapshot(query));
|
|
676
|
+
res.json(await snapshot(query));
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
// Cloudflare ayrı uçlarda: her ikisi de ağa çıkıyor ve döküm turuna
|
|
680
|
+
// bağlanırlarsa panel Cloudflare'in gecikmesi kadar yavaşlar.
|
|
681
|
+
api.get("/cloudflare", async (req, res) => {
|
|
682
|
+
res.json(await fetchCloudflareOverview({ force: req.query.force === "1" }));
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
api.post("/cloudflare/analytics", express.json({ limit: "8kb" }), async (req, res) => {
|
|
686
|
+
if (!cloudflareConfigured()) {
|
|
687
|
+
res.json({ ok: false, error: "not configured" });
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const hours = Number(req.body?.hours) || undefined;
|
|
692
|
+
const path = typeof req.body?.path === "string" ? req.body.path.trim() : "";
|
|
693
|
+
|
|
694
|
+
// Yol verildiyse soru "bu sayfa hangi edge'lerden servis edildi", yoksa
|
|
695
|
+
// "zone genelinde cache oranı ne".
|
|
696
|
+
res.json(path ? await fetchPathEdges({ path, hours }) : await fetchCacheAnalytics({ hours }));
|
|
463
697
|
});
|
|
464
698
|
|
|
465
699
|
api.post("/action", express.json({ limit: "64kb" }), async (req, res) => {
|
|
@@ -479,6 +713,18 @@ function router() {
|
|
|
479
713
|
return api;
|
|
480
714
|
}
|
|
481
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Purge URL'lerinin kökü. `cloudflare.hostname` verilmediğinde panelin
|
|
718
|
+
* açıldığı origin kullanılır: tek zone'lu bir kurulumda ikisi zaten aynı.
|
|
719
|
+
*
|
|
720
|
+
* @param {import('express').Request} req
|
|
721
|
+
* @returns {string | undefined}
|
|
722
|
+
*/
|
|
723
|
+
function originOf(req) {
|
|
724
|
+
const host = req.get("host");
|
|
725
|
+
return host ? `${req.protocol}://${host}` : undefined;
|
|
726
|
+
}
|
|
727
|
+
|
|
482
728
|
/**
|
|
483
729
|
* @param {import('express').Response} res
|
|
484
730
|
* @param {string} name
|
|
@@ -497,9 +743,17 @@ export function mountCachePanel(app) {
|
|
|
497
743
|
|
|
498
744
|
app.use(settings.basePath, router());
|
|
499
745
|
|
|
746
|
+
// Şifre akışın içinde tek bir satır olarak kaybolmamalı: kullanıcı onu bir
|
|
747
|
+
// kez görüyor ve her restart'ta değişiyor.
|
|
500
748
|
const port = process.env.PORT ?? 3000;
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
749
|
+
log.box({
|
|
750
|
+
title: "CACHE PANEL",
|
|
751
|
+
lines: [
|
|
752
|
+
`http://localhost:${port}${settings.basePath}`,
|
|
753
|
+
"",
|
|
754
|
+
`password ${PASSWORD}`,
|
|
755
|
+
"",
|
|
756
|
+
"Valid until this process restarts.",
|
|
757
|
+
],
|
|
758
|
+
});
|
|
505
759
|
}
|