mktcms 0.3.8 → 0.3.10

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mktcms",
3
3
  "configKey": "mktcms",
4
- "version": "0.3.8",
4
+ "version": "0.3.10",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -83,6 +83,11 @@ const module$1 = defineNuxtModule({
83
83
  name: "useForm",
84
84
  as: "useForm",
85
85
  from: resolver.resolve("runtime/app/composables/useForm")
86
+ },
87
+ {
88
+ name: "useTrackTraffic",
89
+ as: "useTrackTraffic",
90
+ from: resolver.resolve("runtime/app/composables/useTrackTraffic")
86
91
  }
87
92
  ]);
88
93
  addServerImports({
@@ -197,6 +202,11 @@ const module$1 = defineNuxtModule({
197
202
  route: "/api/health",
198
203
  handler: resolver.resolve("./runtime/server/api/health")
199
204
  });
205
+ addServerHandler({
206
+ route: "/api/traffic/track",
207
+ method: "post",
208
+ handler: resolver.resolve("./runtime/server/api/traffic/track.post")
209
+ });
200
210
  extendPages((pages) => {
201
211
  pages.push({
202
212
  name: "Admin Dashboard",
@@ -0,0 +1 @@
1
+ export declare function useTrackTraffic(): void;
@@ -0,0 +1,12 @@
1
+ import { onMounted } from "vue";
2
+ export function useTrackTraffic() {
3
+ if (import.meta.server) {
4
+ return;
5
+ }
6
+ onMounted(() => {
7
+ void $fetch("/api/traffic/track", {
8
+ method: "POST"
9
+ }).catch(() => {
10
+ });
11
+ });
12
+ }
@@ -1,8 +1,10 @@
1
1
  import { createError, defineEventHandler, setHeader } from "h3";
2
2
  import { getStorageUsageBytes } from "../utils/storageUsage.js";
3
+ import { getTrafficMetrics } from "../utils/trafficMetrics.js";
3
4
  export default defineEventHandler(async (event) => {
4
5
  try {
5
6
  const storageUsageBytes = await getStorageUsageBytes();
7
+ const { requestsTotal, uniqueIpsTotal } = getTrafficMetrics();
6
8
  setHeader(event, "Content-Type", "text/plain; version=0.0.4; charset=utf-8");
7
9
  return [
8
10
  "# HELP mktcms_up Whether the mktcms API is healthy (1 = healthy).",
@@ -10,7 +12,13 @@ export default defineEventHandler(async (event) => {
10
12
  "mktcms_up 1",
11
13
  "# HELP mktcms_storage_usage_bytes Current .storage directory size in bytes.",
12
14
  "# TYPE mktcms_storage_usage_bytes gauge",
13
- `mktcms_storage_usage_bytes ${storageUsageBytes}`
15
+ `mktcms_storage_usage_bytes ${storageUsageBytes}`,
16
+ "# HELP mktcms_requests_total Total number of counted page requests.",
17
+ "# TYPE mktcms_requests_total counter",
18
+ `mktcms_requests_total ${requestsTotal}`,
19
+ "# HELP mktcms_unique_ips_total Total number of unique visitor IPs seen in memory.",
20
+ "# TYPE mktcms_unique_ips_total counter",
21
+ `mktcms_unique_ips_total ${uniqueIpsTotal}`
14
22
  ].join("\n") + "\n";
15
23
  } catch (error) {
16
24
  throw createError({
@@ -0,0 +1,4 @@
1
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, {
2
+ tracked: boolean;
3
+ }>;
4
+ export default _default;
@@ -0,0 +1,22 @@
1
+ import { defineEventHandler, getRequestIP } from "h3";
2
+ import { incrementTrafficRequests, trackUniqueIp } from "../../utils/trafficMetrics.js";
3
+ function normalizeIp(ip) {
4
+ if (!ip) {
5
+ return void 0;
6
+ }
7
+ const normalizedIp = ip.trim();
8
+ if (normalizedIp.startsWith("::ffff:")) {
9
+ return normalizedIp.slice(7);
10
+ }
11
+ return normalizedIp;
12
+ }
13
+ export default defineEventHandler((event) => {
14
+ const userAgent = event.node.req.headers["user-agent"];
15
+ if (typeof userAgent === "string" && userAgent.includes("Blackbox")) {
16
+ return { tracked: false };
17
+ }
18
+ incrementTrafficRequests();
19
+ const requestIp = normalizeIp(getRequestIP(event, { xForwardedFor: true }) || event.node.req.socket.remoteAddress);
20
+ trackUniqueIp(requestIp);
21
+ return { tracked: true };
22
+ });
@@ -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
- function getState(clientId) {
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
- const state = getState(clientId);
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
- const state = getState(clientId);
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,6 @@
1
+ export declare function incrementTrafficRequests(): void;
2
+ export declare function trackUniqueIp(ip?: string): void;
3
+ export declare function getTrafficMetrics(): {
4
+ requestsTotal: number;
5
+ uniqueIpsTotal: number;
6
+ };
@@ -0,0 +1,23 @@
1
+ const MAX_STORED_IPS = 1e4;
2
+ let requestsTotal = 0;
3
+ let uniqueIpsTotal = 0;
4
+ const seenIps = /* @__PURE__ */ new Set();
5
+ export function incrementTrafficRequests() {
6
+ requestsTotal += 1;
7
+ }
8
+ export function trackUniqueIp(ip) {
9
+ if (!ip || seenIps.has(ip)) {
10
+ return;
11
+ }
12
+ if (seenIps.size >= MAX_STORED_IPS) {
13
+ return;
14
+ }
15
+ seenIps.add(ip);
16
+ uniqueIpsTotal += 1;
17
+ }
18
+ export function getTrafficMetrics() {
19
+ return {
20
+ requestsTotal,
21
+ uniqueIpsTotal
22
+ };
23
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mktcms",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Simple CMS module for Nuxt",
5
5
  "repository": "mktcode/mktcms",
6
6
  "license": "MIT",