mktcms 0.3.7 → 0.3.9
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/dist/module.json +1 -1
- package/dist/module.mjs +4 -0
- package/dist/runtime/server/api/admin/storage-usage.js +3 -81
- package/dist/runtime/server/api/health.d.ts +2 -0
- package/dist/runtime/server/api/health.js +21 -0
- package/dist/runtime/server/utils/loginRateLimit.js +52 -4
- package/dist/runtime/server/utils/storageUsage.d.ts +1 -0
- package/dist/runtime/server/utils/storageUsage.js +44 -0
- package/package.json +1 -1
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -193,6 +193,10 @@ const module$1 = defineNuxtModule({
|
|
|
193
193
|
route: "/api/content/:path",
|
|
194
194
|
handler: resolver.resolve("./runtime/server/api/content/[path]")
|
|
195
195
|
});
|
|
196
|
+
addServerHandler({
|
|
197
|
+
route: "/api/health",
|
|
198
|
+
handler: resolver.resolve("./runtime/server/api/health")
|
|
199
|
+
});
|
|
196
200
|
extendPages((pages) => {
|
|
197
201
|
pages.push({
|
|
198
202
|
name: "Admin Dashboard",
|
|
@@ -1,87 +1,9 @@
|
|
|
1
|
-
import { access, readdir, lstat } from "node:fs/promises";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import { execFile } from "node:child_process";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
1
|
import { createError, defineEventHandler } from "h3";
|
|
6
|
-
|
|
7
|
-
const CACHE_TTL_MS = 6e4;
|
|
8
|
-
let cachedBytes = null;
|
|
9
|
-
let inFlight = null;
|
|
10
|
-
async function getDirectorySizeBytes(dirPath) {
|
|
11
|
-
const dirsToVisit = [dirPath];
|
|
12
|
-
let totalBytes = 0;
|
|
13
|
-
while (dirsToVisit.length > 0) {
|
|
14
|
-
const currentDir = dirsToVisit.pop();
|
|
15
|
-
let entries;
|
|
16
|
-
try {
|
|
17
|
-
entries = await readdir(currentDir, { withFileTypes: true });
|
|
18
|
-
} catch (error) {
|
|
19
|
-
if (error?.code === "ENOENT") {
|
|
20
|
-
continue;
|
|
21
|
-
}
|
|
22
|
-
throw error;
|
|
23
|
-
}
|
|
24
|
-
for (const entry of entries) {
|
|
25
|
-
const entryPath = resolve(currentDir, entry.name);
|
|
26
|
-
let stats;
|
|
27
|
-
try {
|
|
28
|
-
stats = await lstat(entryPath);
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (error?.code === "ENOENT") {
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
throw error;
|
|
34
|
-
}
|
|
35
|
-
if (stats.isDirectory()) {
|
|
36
|
-
dirsToVisit.push(entryPath);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
totalBytes += stats.size;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return totalBytes;
|
|
43
|
-
}
|
|
2
|
+
import { getStorageUsageBytes } from "../../utils/storageUsage.js";
|
|
44
3
|
export default defineEventHandler(async () => {
|
|
45
4
|
try {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
return { bytes: cachedBytes.value };
|
|
49
|
-
}
|
|
50
|
-
const storageDir = resolve(process.cwd(), ".storage");
|
|
51
|
-
try {
|
|
52
|
-
await access(storageDir);
|
|
53
|
-
} catch {
|
|
54
|
-
cachedBytes = { value: 0, at: now };
|
|
55
|
-
return { bytes: 0 };
|
|
56
|
-
}
|
|
57
|
-
try {
|
|
58
|
-
if (!inFlight) {
|
|
59
|
-
inFlight = (async () => {
|
|
60
|
-
try {
|
|
61
|
-
const { stdout } = await execFileAsync("du", ["-sb", storageDir], {
|
|
62
|
-
timeout: 15e3,
|
|
63
|
-
maxBuffer: 1024 * 1024
|
|
64
|
-
});
|
|
65
|
-
const bytesString = stdout.trim().split(/\s+/)[0];
|
|
66
|
-
const bytes2 = Number.parseInt(bytesString || "", 10);
|
|
67
|
-
if (!Number.isFinite(bytes2) || bytes2 < 0) {
|
|
68
|
-
throw new Error("Unexpected du output");
|
|
69
|
-
}
|
|
70
|
-
return bytes2;
|
|
71
|
-
} catch {
|
|
72
|
-
return await getDirectorySizeBytes(storageDir);
|
|
73
|
-
}
|
|
74
|
-
})();
|
|
75
|
-
}
|
|
76
|
-
const bytes = await inFlight;
|
|
77
|
-
cachedBytes = { value: bytes, at: Date.now() };
|
|
78
|
-
return { bytes };
|
|
79
|
-
} catch {
|
|
80
|
-
inFlight = null;
|
|
81
|
-
throw new Error("Failed to calculate storage usage");
|
|
82
|
-
} finally {
|
|
83
|
-
inFlight = null;
|
|
84
|
-
}
|
|
5
|
+
const bytes = await getStorageUsageBytes();
|
|
6
|
+
return { bytes };
|
|
85
7
|
} catch (error) {
|
|
86
8
|
throw createError({
|
|
87
9
|
statusCode: 500,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createError, defineEventHandler, setHeader } from "h3";
|
|
2
|
+
import { getStorageUsageBytes } from "../utils/storageUsage.js";
|
|
3
|
+
export default defineEventHandler(async (event) => {
|
|
4
|
+
try {
|
|
5
|
+
const storageUsageBytes = await getStorageUsageBytes();
|
|
6
|
+
setHeader(event, "Content-Type", "text/plain; version=0.0.4; charset=utf-8");
|
|
7
|
+
return [
|
|
8
|
+
"# HELP mktcms_up Whether the mktcms API is healthy (1 = healthy).",
|
|
9
|
+
"# TYPE mktcms_up gauge",
|
|
10
|
+
"mktcms_up 1",
|
|
11
|
+
"# HELP mktcms_storage_usage_bytes Current .storage directory size in bytes.",
|
|
12
|
+
"# TYPE mktcms_storage_usage_bytes gauge",
|
|
13
|
+
`mktcms_storage_usage_bytes ${storageUsageBytes}`
|
|
14
|
+
].join("\n") + "\n";
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: 500,
|
|
18
|
+
statusMessage: error?.message || "Failed to build health metrics"
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
});
|
|
@@ -1,14 +1,58 @@
|
|
|
1
1
|
import { createError, getRequestIP } from "h3";
|
|
2
2
|
import { useRuntimeConfig } from "nitropack/runtime";
|
|
3
3
|
const attempts = /* @__PURE__ */ new Map();
|
|
4
|
-
|
|
4
|
+
const SWEEP_EVERY_ACCESSES = 64;
|
|
5
|
+
const INACTIVE_GRACE_MS = 60 * 60 * 1e3;
|
|
6
|
+
const MAX_TRACKED_CLIENTS = 1e4;
|
|
7
|
+
let accessCount = 0;
|
|
8
|
+
function isInactiveState(state, now) {
|
|
9
|
+
return state.blockedUntil <= now && now - state.lastSeenAt > INACTIVE_GRACE_MS;
|
|
10
|
+
}
|
|
11
|
+
function evictInactiveClients(now) {
|
|
12
|
+
const toDelete = [];
|
|
13
|
+
attempts.forEach((state, clientId) => {
|
|
14
|
+
if (isInactiveState(state, now)) {
|
|
15
|
+
toDelete.push(clientId);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
for (const clientId of toDelete) {
|
|
19
|
+
attempts.delete(clientId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function enforceTrackedClientsCap() {
|
|
23
|
+
while (attempts.size > MAX_TRACKED_CLIENTS) {
|
|
24
|
+
let oldestClientId;
|
|
25
|
+
attempts.forEach((_state, clientId) => {
|
|
26
|
+
if (!oldestClientId) {
|
|
27
|
+
oldestClientId = clientId;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
if (!oldestClientId) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
attempts.delete(oldestClientId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function maybeSweep(now) {
|
|
37
|
+
accessCount += 1;
|
|
38
|
+
if (accessCount % SWEEP_EVERY_ACCESSES !== 0) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
evictInactiveClients(now);
|
|
42
|
+
enforceTrackedClientsCap();
|
|
43
|
+
}
|
|
44
|
+
function getState(clientId, now) {
|
|
5
45
|
const state = attempts.get(clientId);
|
|
6
46
|
if (state) {
|
|
47
|
+
state.lastSeenAt = now;
|
|
48
|
+
attempts.delete(clientId);
|
|
49
|
+
attempts.set(clientId, state);
|
|
7
50
|
return state;
|
|
8
51
|
}
|
|
9
52
|
const next = {
|
|
10
53
|
failedAt: [],
|
|
11
|
-
blockedUntil: 0
|
|
54
|
+
blockedUntil: 0,
|
|
55
|
+
lastSeenAt: now
|
|
12
56
|
};
|
|
13
57
|
attempts.set(clientId, next);
|
|
14
58
|
return next;
|
|
@@ -41,7 +85,8 @@ export function assertLoginNotRateLimited(event) {
|
|
|
41
85
|
const { windowMs } = getRateLimitSettings(event);
|
|
42
86
|
const clientId = getClientId(event);
|
|
43
87
|
const now = Date.now();
|
|
44
|
-
|
|
88
|
+
maybeSweep(now);
|
|
89
|
+
const state = getState(clientId, now);
|
|
45
90
|
clearStaleAttempts(state, now, windowMs);
|
|
46
91
|
if (state.blockedUntil > now) {
|
|
47
92
|
const retryAfterSeconds = Math.ceil((state.blockedUntil - now) / 1e3);
|
|
@@ -58,7 +103,8 @@ export function recordFailedLoginAttempt(event) {
|
|
|
58
103
|
const { maxAttempts, windowMs, blockMs } = getRateLimitSettings(event);
|
|
59
104
|
const clientId = getClientId(event);
|
|
60
105
|
const now = Date.now();
|
|
61
|
-
|
|
106
|
+
maybeSweep(now);
|
|
107
|
+
const state = getState(clientId, now);
|
|
62
108
|
clearStaleAttempts(state, now, windowMs);
|
|
63
109
|
state.failedAt.push(now);
|
|
64
110
|
if (state.failedAt.length >= maxAttempts) {
|
|
@@ -67,6 +113,8 @@ export function recordFailedLoginAttempt(event) {
|
|
|
67
113
|
}
|
|
68
114
|
}
|
|
69
115
|
export function clearFailedLoginAttempts(event) {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
maybeSweep(now);
|
|
70
118
|
const clientId = getClientId(event);
|
|
71
119
|
attempts.delete(clientId);
|
|
72
120
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getStorageUsageBytes(): Promise<number>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { execFile } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const CACHE_TTL_MS = 9e5;
|
|
7
|
+
let cachedBytes = null;
|
|
8
|
+
let inFlight = null;
|
|
9
|
+
export async function getStorageUsageBytes() {
|
|
10
|
+
const now = Date.now();
|
|
11
|
+
if (cachedBytes && now - cachedBytes.at < CACHE_TTL_MS) {
|
|
12
|
+
return cachedBytes.value;
|
|
13
|
+
}
|
|
14
|
+
const storageDir = resolve(process.cwd(), ".storage");
|
|
15
|
+
try {
|
|
16
|
+
await access(storageDir);
|
|
17
|
+
} catch {
|
|
18
|
+
cachedBytes = { value: 0, at: now };
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
if (!inFlight) {
|
|
23
|
+
inFlight = (async () => {
|
|
24
|
+
const { stdout } = await execFileAsync("du", ["-sb", storageDir], {
|
|
25
|
+
timeout: 15e3,
|
|
26
|
+
maxBuffer: 1024 * 1024
|
|
27
|
+
});
|
|
28
|
+
const bytesString = stdout.trim().split(/\s+/)[0];
|
|
29
|
+
const bytes2 = Number.parseInt(bytesString || "", 10);
|
|
30
|
+
if (!Number.isFinite(bytes2) || bytes2 < 0) {
|
|
31
|
+
throw new Error("Unexpected du output");
|
|
32
|
+
}
|
|
33
|
+
return bytes2;
|
|
34
|
+
})();
|
|
35
|
+
}
|
|
36
|
+
const bytes = await inFlight;
|
|
37
|
+
cachedBytes = { value: bytes, at: Date.now() };
|
|
38
|
+
return bytes;
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error("Failed to calculate storage usage");
|
|
41
|
+
} finally {
|
|
42
|
+
inFlight = null;
|
|
43
|
+
}
|
|
44
|
+
}
|