iobroker.time-tracker 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +386 -0
- package/admin/i18n/de.json +34 -0
- package/admin/i18n/en.json +1 -0
- package/admin/i18n/es.json +34 -0
- package/admin/i18n/fr.json +34 -0
- package/admin/i18n/it.json +34 -0
- package/admin/i18n/nl.json +34 -0
- package/admin/i18n/pl.json +34 -0
- package/admin/i18n/pt.json +34 -0
- package/admin/i18n/ru.json +34 -0
- package/admin/i18n/uk.json +34 -0
- package/admin/i18n/zh-cn.json +34 -0
- package/admin/jsonConfig.json +266 -0
- package/admin/time-tracker.png +0 -0
- package/build/lib/adapter/automation.js +100 -0
- package/build/lib/adapter/automation.js.map +7 -0
- package/build/lib/adapter/commands.js +160 -0
- package/build/lib/adapter/commands.js.map +7 -0
- package/build/lib/adapter/messages.js +207 -0
- package/build/lib/adapter/messages.js.map +7 -0
- package/build/lib/adapter/presence.js +129 -0
- package/build/lib/adapter/presence.js.map +7 -0
- package/build/lib/adapter/states.js +330 -0
- package/build/lib/adapter/states.js.map +7 -0
- package/build/lib/adapter/triggers.js +92 -0
- package/build/lib/adapter/triggers.js.map +7 -0
- package/build/lib/db/database.js +96 -0
- package/build/lib/db/database.js.map +7 -0
- package/build/lib/db/migrations.js +603 -0
- package/build/lib/db/migrations.js.map +7 -0
- package/build/lib/db/repositories/absences.js +432 -0
- package/build/lib/db/repositories/absences.js.map +7 -0
- package/build/lib/db/repositories/audit.js +90 -0
- package/build/lib/db/repositories/audit.js.map +7 -0
- package/build/lib/db/repositories/automations.js +326 -0
- package/build/lib/db/repositories/automations.js.map +7 -0
- package/build/lib/db/repositories/entries.js +335 -0
- package/build/lib/db/repositories/entries.js.map +7 -0
- package/build/lib/db/repositories/holidays.js +156 -0
- package/build/lib/db/repositories/holidays.js.map +7 -0
- package/build/lib/db/repositories/payouts.js +221 -0
- package/build/lib/db/repositories/payouts.js.map +7 -0
- package/build/lib/db/repositories/rfid.js +264 -0
- package/build/lib/db/repositories/rfid.js.map +7 -0
- package/build/lib/db/repositories/rules.js +356 -0
- package/build/lib/db/repositories/rules.js.map +7 -0
- package/build/lib/db/repositories/settings.js +91 -0
- package/build/lib/db/repositories/settings.js.map +7 -0
- package/build/lib/db/repositories/terminals.js +243 -0
- package/build/lib/db/repositories/terminals.js.map +7 -0
- package/build/lib/db/repositories/triggers.js +240 -0
- package/build/lib/db/repositories/triggers.js.map +7 -0
- package/build/lib/db/repositories/users.js +491 -0
- package/build/lib/db/repositories/users.js.map +7 -0
- package/build/lib/db/seed.js +180 -0
- package/build/lib/db/seed.js.map +7 -0
- package/build/lib/domain/avatar.js +63 -0
- package/build/lib/domain/avatar.js.map +7 -0
- package/build/lib/domain/breaks.js +70 -0
- package/build/lib/domain/breaks.js.map +7 -0
- package/build/lib/domain/calculation.js +150 -0
- package/build/lib/domain/calculation.js.map +7 -0
- package/build/lib/domain/holidays.js +113 -0
- package/build/lib/domain/holidays.js.map +7 -0
- package/build/lib/domain/punch.js +99 -0
- package/build/lib/domain/punch.js.map +7 -0
- package/build/lib/domain/target.js +89 -0
- package/build/lib/domain/target.js.map +7 -0
- package/build/lib/errors.js +56 -0
- package/build/lib/errors.js.map +7 -0
- package/build/lib/reports/labels.js +358 -0
- package/build/lib/reports/labels.js.map +7 -0
- package/build/lib/reports/pdf.js +308 -0
- package/build/lib/reports/pdf.js.map +7 -0
- package/build/lib/reports/types.js +32 -0
- package/build/lib/reports/types.js.map +7 -0
- package/build/lib/reports/xls.js +211 -0
- package/build/lib/reports/xls.js.map +7 -0
- package/build/lib/services/aggregation.js +495 -0
- package/build/lib/services/aggregation.js.map +7 -0
- package/build/lib/services/auth.js +381 -0
- package/build/lib/services/auth.js.map +7 -0
- package/build/lib/services/backup.js +372 -0
- package/build/lib/services/backup.js.map +7 -0
- package/build/lib/services/closing.js +106 -0
- package/build/lib/services/closing.js.map +7 -0
- package/build/lib/services/sessionSecret.js +100 -0
- package/build/lib/services/sessionSecret.js.map +7 -0
- package/build/lib/services/sync.js +278 -0
- package/build/lib/services/sync.js.map +7 -0
- package/build/lib/util/time.js +125 -0
- package/build/lib/util/time.js.map +7 -0
- package/build/lib/web/api.js +2589 -0
- package/build/lib/web/api.js.map +7 -0
- package/build/lib/web/cookies.js +80 -0
- package/build/lib/web/cookies.js.map +7 -0
- package/build/lib/web/events.js +50 -0
- package/build/lib/web/events.js.map +7 -0
- package/build/lib/web/pin-guard.js +86 -0
- package/build/lib/web/pin-guard.js.map +7 -0
- package/build/lib/web/problem.js +129 -0
- package/build/lib/web/problem.js.map +7 -0
- package/build/lib/web/rate-limit.js +65 -0
- package/build/lib/web/rate-limit.js.map +7 -0
- package/build/lib/web/router.js +274 -0
- package/build/lib/web/router.js.map +7 -0
- package/build/lib/web/server.js +189 -0
- package/build/lib/web/server.js.map +7 -0
- package/build/lib/web/static.js +151 -0
- package/build/lib/web/static.js.map +7 -0
- package/build/lib/web/stream.js +135 -0
- package/build/lib/web/stream.js.map +7 -0
- package/build/main.js +986 -0
- package/build/main.js.map +7 -0
- package/io-package.json +221 -0
- package/package.json +131 -0
- package/src-pwa/src/i18n/de.json +359 -0
- package/src-pwa/src/i18n/en.json +359 -0
- package/src-pwa/src/i18n/es.json +359 -0
- package/src-pwa/src/i18n/fr.json +359 -0
- package/src-pwa/src/i18n/index.ts +96 -0
- package/src-pwa/src/i18n/it.json +359 -0
- package/src-pwa/src/i18n/nl.json +359 -0
- package/src-pwa/src/i18n/pl.json +359 -0
- package/src-pwa/src/i18n/pt.json +359 -0
- package/src-pwa/src/i18n/ru.json +359 -0
- package/src-pwa/src/i18n/uk.json +359 -0
- package/src-pwa/src/i18n/zh-cn.json +359 -0
- package/www/assets/index-QI3wrEvW.js +119 -0
- package/www/assets/index-QI3wrEvW.js.map +1 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
- package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
- package/www/favicon.svg +16 -0
- package/www/icon-192.png +0 -0
- package/www/icon-512.png +0 -0
- package/www/index.html +38 -0
- package/www/manifest.webmanifest +1 -0
- package/www/person.png +0 -0
- package/www/sw.js +3 -0
- package/www/sw.js.map +1 -0
- package/www/workbox-9c191d2f.js +3 -0
- package/www/workbox-9c191d2f.js.map +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var events_exports = {};
|
|
20
|
+
__export(events_exports, {
|
|
21
|
+
createEventBus: () => createEventBus
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(events_exports);
|
|
24
|
+
function createEventBus() {
|
|
25
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
26
|
+
return {
|
|
27
|
+
publish(event) {
|
|
28
|
+
for (const listener of [...listeners]) {
|
|
29
|
+
try {
|
|
30
|
+
listener(event);
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
subscribe(listener) {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
listenerCount() {
|
|
42
|
+
return listeners.size;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
47
|
+
0 && (module.exports = {
|
|
48
|
+
createEventBus
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/web/events.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Live events of the adapter.\n *\n * Everything that changes data publishes one event here: a punch, a correction, an import, a closed month. The\n * WebSocket endpoint (`/api/stream`) forwards them to connected clients, and the adapter uses the same bus to\n * refresh its own states. The bus is deliberately tiny: an in-memory fan-out without history, because a client\n * that missed an event reloads the data it cares about anyway.\n */\n\n/** Type of a live event. */\nexport type EventType =\n\t| \"punch\"\n\t| \"entry.update\"\n\t| \"entry.delete\"\n\t| \"entries.import\"\n\t| \"absence.change\"\n\t| \"month.close\"\n\t| \"terminal.punch\"\n\t| \"rfid.scan\"\n\t| \"backup.create\"\n\t| \"automation.clockIn\"\n\t| \"automation.clockOut\"\n\t| \"automation.missingPunch\"\n\t| \"automation.breakReminder\";\n\n/** One live event. */\nexport interface ApiEvent {\n\t/** Kind of the change */\n\ttype: EventType;\n\t/** Instant of the change, UTC epoch seconds */\n\tatUtc: number;\n\t/** Employee the change belongs to, `null` for instance wide events */\n\tuserId: number | null;\n\t/** Short, client readable summary (never personal data beyond the id) */\n\tdata?: Record<string, unknown>;\n}\n\n/** A subscriber of the bus. */\nexport type EventListener = (event: ApiEvent) => void;\n\n/** The event bus. */\nexport interface EventBus {\n\t/** Publishes an event to every listener */\n\tpublish(event: ApiEvent): void;\n\t/** Registers a listener and returns the function that removes it again */\n\tsubscribe(listener: EventListener): () => void;\n\t/** Number of connected listeners */\n\tlistenerCount(): number;\n}\n\n/**\n * Creates an event bus.\n *\n * @returns the bus\n */\nexport function createEventBus(): EventBus {\n\tconst listeners = new Set<EventListener>();\n\n\treturn {\n\t\tpublish(event: ApiEvent): void {\n\t\t\t// a listener that throws must not break the others (or the request that published)\n\t\t\tfor (const listener of [...listeners]) {\n\t\t\t\ttry {\n\t\t\t\t\tlistener(event);\n\t\t\t\t} catch {\n\t\t\t\t\t// ignored on purpose: the event is a notification, not a transaction\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\n\t\tsubscribe(listener: EventListener): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\n\t\tlistenerCount(): number {\n\t\t\treturn listeners.size;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAuDO,SAAS,iBAA2B;AAC1C,QAAM,YAAY,oBAAI,IAAmB;AAEzC,SAAO;AAAA,IACN,QAAQ,OAAuB;AAE9B,iBAAW,YAAY,CAAC,GAAG,SAAS,GAAG;AACtC,YAAI;AACH,mBAAS,KAAK;AAAA,QACf,QAAQ;AAAA,QAER;AAAA,MACD;AAAA,IACD;AAAA,IAEA,UAAU,UAAqC;AAC9C,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM;AACZ,kBAAU,OAAO,QAAQ;AAAA,MAC1B;AAAA,IACD;AAAA,IAEA,gBAAwB;AACvB,aAAO,UAAU;AAAA,IAClB;AAAA,EACD;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var pin_guard_exports = {};
|
|
20
|
+
__export(pin_guard_exports, {
|
|
21
|
+
createPinGuard: () => createPinGuard
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(pin_guard_exports);
|
|
24
|
+
function createPinGuard(options = {}) {
|
|
25
|
+
var _a, _b;
|
|
26
|
+
const maxFailures = Math.max(1, (_a = options.maxFailures) != null ? _a : 5);
|
|
27
|
+
const lockSeconds = Math.max(1, (_b = options.lockSeconds) != null ? _b : 15 * 60);
|
|
28
|
+
const entries = /* @__PURE__ */ new Map();
|
|
29
|
+
const prune = (now) => {
|
|
30
|
+
for (const [userId, entry] of entries) {
|
|
31
|
+
const over = now >= entry.lockedUntil && now - entry.windowStartedAt >= lockSeconds;
|
|
32
|
+
if (over) {
|
|
33
|
+
entries.delete(userId);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const state = (userId, now) => {
|
|
38
|
+
const entry = entries.get(userId);
|
|
39
|
+
if (!entry) {
|
|
40
|
+
return { locked: false, retryAfterSeconds: 0, failures: 0 };
|
|
41
|
+
}
|
|
42
|
+
if (now < entry.lockedUntil) {
|
|
43
|
+
return { locked: true, retryAfterSeconds: entry.lockedUntil - now, failures: entry.failures };
|
|
44
|
+
}
|
|
45
|
+
if (now - entry.windowStartedAt >= lockSeconds) {
|
|
46
|
+
entries.delete(userId);
|
|
47
|
+
return { locked: false, retryAfterSeconds: 0, failures: 0 };
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
locked: false,
|
|
51
|
+
retryAfterSeconds: lockSeconds - (now - entry.windowStartedAt),
|
|
52
|
+
failures: entry.failures
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
state: ({ userId, now = Math.floor(Date.now() / 1e3) }) => state(userId, now),
|
|
57
|
+
fail: ({ userId, now = Math.floor(Date.now() / 1e3) }) => {
|
|
58
|
+
var _a2, _b2;
|
|
59
|
+
const current = state(userId, now);
|
|
60
|
+
if (current.locked) {
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
63
|
+
if (entries.size > 1e3) {
|
|
64
|
+
prune(now);
|
|
65
|
+
}
|
|
66
|
+
const entry = entries.get(userId);
|
|
67
|
+
const failures = ((_a2 = entry == null ? void 0 : entry.failures) != null ? _a2 : 0) + 1;
|
|
68
|
+
const windowStartedAt = (_b2 = entry == null ? void 0 : entry.windowStartedAt) != null ? _b2 : now;
|
|
69
|
+
if (failures >= maxFailures) {
|
|
70
|
+
entries.set(userId, { failures, windowStartedAt, lockedUntil: now + lockSeconds });
|
|
71
|
+
return { locked: true, retryAfterSeconds: lockSeconds, failures };
|
|
72
|
+
}
|
|
73
|
+
entries.set(userId, { failures, windowStartedAt, lockedUntil: 0 });
|
|
74
|
+
return { locked: false, retryAfterSeconds: 0, failures };
|
|
75
|
+
},
|
|
76
|
+
reset: (userId) => {
|
|
77
|
+
entries.delete(userId);
|
|
78
|
+
},
|
|
79
|
+
size: () => entries.size
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
83
|
+
0 && (module.exports = {
|
|
84
|
+
createPinGuard
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=pin-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/web/pin-guard.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Failure counter for the badge PIN at the kiosk terminal (specification 4.10).\n *\n * A four digit PIN is guessed in a few hundred attempts, and a terminal stands in a shared room: the IP rate\n * limit of the route alone is not enough, because everybody uses the same device. This guard therefore counts\n * the wrong PINs **per account** and blocks that account for a while after too many of them \u2014 exactly like the\n * login lockout, only much shorter, since a fat finger is more likely than an attack.\n *\n * The counters live in memory: a restart of the adapter clears them, which is documented and acceptable because\n * the block is a short lived brake, not a permanent lock. (The login lockout, which must survive a restart, is\n * kept in the database instead.)\n */\n\n/** Options of the guard. */\nexport interface PinGuardOptions {\n\t/** Wrong PINs that are allowed inside one window (default 5) */\n\tmaxFailures?: number;\n\t/** Length of the window and of the block in seconds (default 900 = 15 minutes) */\n\tlockSeconds?: number;\n}\n\n/** Result of a check. */\nexport interface PinGuardState {\n\t/** True while the account is blocked */\n\tlocked: boolean;\n\t/** Seconds until the block ends or the window starts over */\n\tretryAfterSeconds: number;\n\t/** Wrong PINs inside the current window */\n\tfailures: number;\n}\n\n/** The guard. */\nexport interface PinGuard {\n\t/** Reads the state of an account without changing it */\n\tstate(input: { userId: number; now?: number }): PinGuardState;\n\t/** Counts a wrong PIN and returns the state afterwards */\n\tfail(input: { userId: number; now?: number }): PinGuardState;\n\t/** Forgets the wrong PINs of an account (called after a successful punch) */\n\treset(userId: number): void;\n\t/** Number of accounts with a recorded failure (for diagnostics) */\n\tsize(): number;\n}\n\n/** What is remembered per account. */\ninterface Entry {\n\t/** Wrong PINs since the window started */\n\tfailures: number;\n\t/** Instant the window started, UTC epoch seconds */\n\twindowStartedAt: number;\n\t/** Instant the block ends, `0` while the account is not blocked */\n\tlockedUntil: number;\n}\n\n/**\n * Creates the guard.\n *\n * @param options - threshold and window\n * @returns the guard\n */\nexport function createPinGuard(options: PinGuardOptions = {}): PinGuard {\n\tconst maxFailures = Math.max(1, options.maxFailures ?? 5);\n\tconst lockSeconds = Math.max(1, options.lockSeconds ?? 15 * 60);\n\tconst entries = new Map<number, Entry>();\n\n\t/**\n\t * Drops entries whose window and block are over.\n\t *\n\t * @param now - current instant\n\t */\n\tconst prune = (now: number): void => {\n\t\tfor (const [userId, entry] of entries) {\n\t\t\tconst over = now >= entry.lockedUntil && now - entry.windowStartedAt >= lockSeconds;\n\t\t\tif (over) {\n\t\t\t\tentries.delete(userId);\n\t\t\t}\n\t\t}\n\t};\n\n\t/**\n\t * Reads the state, resetting an expired window on the way.\n\t *\n\t * @param userId - account\n\t * @param now - current instant\n\t * @returns the state\n\t */\n\tconst state = (userId: number, now: number): PinGuardState => {\n\t\tconst entry = entries.get(userId);\n\t\tif (!entry) {\n\t\t\treturn { locked: false, retryAfterSeconds: 0, failures: 0 };\n\t\t}\n\t\tif (now < entry.lockedUntil) {\n\t\t\treturn { locked: true, retryAfterSeconds: entry.lockedUntil - now, failures: entry.failures };\n\t\t}\n\t\tif (now - entry.windowStartedAt >= lockSeconds) {\n\t\t\t// the window is over: the next attempt starts from zero\n\t\t\tentries.delete(userId);\n\t\t\treturn { locked: false, retryAfterSeconds: 0, failures: 0 };\n\t\t}\n\t\treturn {\n\t\t\tlocked: false,\n\t\t\tretryAfterSeconds: lockSeconds - (now - entry.windowStartedAt),\n\t\t\tfailures: entry.failures,\n\t\t};\n\t};\n\n\treturn {\n\t\tstate: ({ userId, now = Math.floor(Date.now() / 1000) }) => state(userId, now),\n\n\t\tfail: ({ userId, now = Math.floor(Date.now() / 1000) }) => {\n\t\t\tconst current = state(userId, now);\n\t\t\tif (current.locked) {\n\t\t\t\treturn current;\n\t\t\t}\n\t\t\tif (entries.size > 1000) {\n\t\t\t\tprune(now);\n\t\t\t}\n\t\t\tconst entry = entries.get(userId);\n\t\t\tconst failures = (entry?.failures ?? 0) + 1;\n\t\t\tconst windowStartedAt = entry?.windowStartedAt ?? now;\n\t\t\tif (failures >= maxFailures) {\n\t\t\t\tentries.set(userId, { failures, windowStartedAt, lockedUntil: now + lockSeconds });\n\t\t\t\treturn { locked: true, retryAfterSeconds: lockSeconds, failures };\n\t\t\t}\n\t\t\tentries.set(userId, { failures, windowStartedAt, lockedUntil: 0 });\n\t\t\treturn { locked: false, retryAfterSeconds: 0, failures };\n\t\t},\n\n\t\treset: (userId: number) => {\n\t\t\tentries.delete(userId);\n\t\t},\n\n\t\tsize: () => entries.size,\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2DO,SAAS,eAAe,UAA2B,CAAC,GAAa;AA3DxE;AA4DC,QAAM,cAAc,KAAK,IAAI,IAAG,aAAQ,gBAAR,YAAuB,CAAC;AACxD,QAAM,cAAc,KAAK,IAAI,IAAG,aAAQ,gBAAR,YAAuB,KAAK,EAAE;AAC9D,QAAM,UAAU,oBAAI,IAAmB;AAOvC,QAAM,QAAQ,CAAC,QAAsB;AACpC,eAAW,CAAC,QAAQ,KAAK,KAAK,SAAS;AACtC,YAAM,OAAO,OAAO,MAAM,eAAe,MAAM,MAAM,mBAAmB;AACxE,UAAI,MAAM;AACT,gBAAQ,OAAO,MAAM;AAAA,MACtB;AAAA,IACD;AAAA,EACD;AASA,QAAM,QAAQ,CAAC,QAAgB,QAA+B;AAC7D,UAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,QAAI,CAAC,OAAO;AACX,aAAO,EAAE,QAAQ,OAAO,mBAAmB,GAAG,UAAU,EAAE;AAAA,IAC3D;AACA,QAAI,MAAM,MAAM,aAAa;AAC5B,aAAO,EAAE,QAAQ,MAAM,mBAAmB,MAAM,cAAc,KAAK,UAAU,MAAM,SAAS;AAAA,IAC7F;AACA,QAAI,MAAM,MAAM,mBAAmB,aAAa;AAE/C,cAAQ,OAAO,MAAM;AACrB,aAAO,EAAE,QAAQ,OAAO,mBAAmB,GAAG,UAAU,EAAE;AAAA,IAC3D;AACA,WAAO;AAAA,MACN,QAAQ;AAAA,MACR,mBAAmB,eAAe,MAAM,MAAM;AAAA,MAC9C,UAAU,MAAM;AAAA,IACjB;AAAA,EACD;AAEA,SAAO;AAAA,IACN,OAAO,CAAC,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,EAAE,MAAM,MAAM,QAAQ,GAAG;AAAA,IAE7E,MAAM,CAAC,EAAE,QAAQ,MAAM,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,EAAE,MAAM;AA5G7D,UAAAA,KAAAC;AA6GG,YAAM,UAAU,MAAM,QAAQ,GAAG;AACjC,UAAI,QAAQ,QAAQ;AACnB,eAAO;AAAA,MACR;AACA,UAAI,QAAQ,OAAO,KAAM;AACxB,cAAM,GAAG;AAAA,MACV;AACA,YAAM,QAAQ,QAAQ,IAAI,MAAM;AAChC,YAAM,aAAYD,MAAA,+BAAO,aAAP,OAAAA,MAAmB,KAAK;AAC1C,YAAM,mBAAkBC,MAAA,+BAAO,oBAAP,OAAAA,MAA0B;AAClD,UAAI,YAAY,aAAa;AAC5B,gBAAQ,IAAI,QAAQ,EAAE,UAAU,iBAAiB,aAAa,MAAM,YAAY,CAAC;AACjF,eAAO,EAAE,QAAQ,MAAM,mBAAmB,aAAa,SAAS;AAAA,MACjE;AACA,cAAQ,IAAI,QAAQ,EAAE,UAAU,iBAAiB,aAAa,EAAE,CAAC;AACjE,aAAO,EAAE,QAAQ,OAAO,mBAAmB,GAAG,SAAS;AAAA,IACxD;AAAA,IAEA,OAAO,CAAC,WAAmB;AAC1B,cAAQ,OAAO,MAAM;AAAA,IACtB;AAAA,IAEA,MAAM,MAAM,QAAQ;AAAA,EACrB;AACD;",
|
|
6
|
+
"names": ["_a", "_b"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var problem_exports = {};
|
|
20
|
+
__export(problem_exports, {
|
|
21
|
+
HttpProblem: () => HttpProblem,
|
|
22
|
+
PROBLEM_CONTENT_TYPE: () => PROBLEM_CONTENT_TYPE,
|
|
23
|
+
problem: () => problem,
|
|
24
|
+
toProblem: () => toProblem
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(problem_exports);
|
|
27
|
+
var import_entries = require("../db/repositories/entries");
|
|
28
|
+
var import_users = require("../db/repositories/users");
|
|
29
|
+
var import_absences = require("../db/repositories/absences");
|
|
30
|
+
var import_errors = require("../errors");
|
|
31
|
+
const PROBLEM_CONTENT_TYPE = "application/problem+json; charset=utf-8";
|
|
32
|
+
const TITLES = {
|
|
33
|
+
400: "Bad Request",
|
|
34
|
+
401: "Unauthorized",
|
|
35
|
+
403: "Forbidden",
|
|
36
|
+
404: "Not Found",
|
|
37
|
+
405: "Method Not Allowed",
|
|
38
|
+
409: "Conflict",
|
|
39
|
+
413: "Payload Too Large",
|
|
40
|
+
415: "Unsupported Media Type",
|
|
41
|
+
423: "Locked",
|
|
42
|
+
429: "Too Many Requests",
|
|
43
|
+
500: "Internal Server Error"
|
|
44
|
+
};
|
|
45
|
+
class HttpProblem extends Error {
|
|
46
|
+
/**
|
|
47
|
+
* Creates the error.
|
|
48
|
+
*
|
|
49
|
+
* @param status - HTTP status code
|
|
50
|
+
* @param code - stable error code
|
|
51
|
+
* @param detail - optional explanation
|
|
52
|
+
* @param title - optional title (defaults to the reason phrase of the status)
|
|
53
|
+
* @param fields - optional list of invalid fields
|
|
54
|
+
*/
|
|
55
|
+
constructor(status, code, detail, title, fields = []) {
|
|
56
|
+
var _a;
|
|
57
|
+
super(`${status} ${code}${detail ? `: ${detail}` : ""}`);
|
|
58
|
+
this.status = status;
|
|
59
|
+
this.code = code;
|
|
60
|
+
this.detail = detail;
|
|
61
|
+
this.fields = fields;
|
|
62
|
+
this.name = "HttpProblem";
|
|
63
|
+
this.problemTitle = (_a = title != null ? title : TITLES[status]) != null ? _a : "Error";
|
|
64
|
+
}
|
|
65
|
+
/** Title used in the problem document. */
|
|
66
|
+
problemTitle;
|
|
67
|
+
/**
|
|
68
|
+
* Converts the error into a problem document.
|
|
69
|
+
*
|
|
70
|
+
* @param instance - request path the problem occurred on
|
|
71
|
+
* @returns problem document
|
|
72
|
+
*/
|
|
73
|
+
toProblem(instance) {
|
|
74
|
+
return {
|
|
75
|
+
type: "about:blank",
|
|
76
|
+
title: this.problemTitle,
|
|
77
|
+
status: this.status,
|
|
78
|
+
code: this.code,
|
|
79
|
+
...this.detail ? { detail: this.detail } : {},
|
|
80
|
+
...this.fields.length > 0 ? { errors: this.fields.map((field) => ({ path: field.path, message: field.message })) } : {},
|
|
81
|
+
...instance ? { instance } : {}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function problem(status, code, detail) {
|
|
86
|
+
return new HttpProblem(status, code, detail);
|
|
87
|
+
}
|
|
88
|
+
function toProblem(error, instance) {
|
|
89
|
+
if (error instanceof HttpProblem) {
|
|
90
|
+
return { problem: error.toProblem(instance), unexpected: false };
|
|
91
|
+
}
|
|
92
|
+
if (error instanceof import_errors.ValidationError) {
|
|
93
|
+
return {
|
|
94
|
+
problem: new HttpProblem(400, "bad_request", error.message, void 0, error.fields).toProblem(instance),
|
|
95
|
+
unexpected: false
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
if (error instanceof import_errors.NotFoundError) {
|
|
99
|
+
return { problem: new HttpProblem(404, "not_found", error.message).toProblem(instance), unexpected: false };
|
|
100
|
+
}
|
|
101
|
+
if (error instanceof import_entries.RevisionConflictError) {
|
|
102
|
+
return {
|
|
103
|
+
problem: new HttpProblem(
|
|
104
|
+
409,
|
|
105
|
+
"revision_conflict",
|
|
106
|
+
`the record was changed by someone else (current revision ${error.current.revision})`
|
|
107
|
+
).toProblem(instance),
|
|
108
|
+
unexpected: false
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (error instanceof import_users.LoginExistsError || error instanceof import_users.UnknownRoleError) {
|
|
112
|
+
return { problem: new HttpProblem(409, "conflict", error.message).toProblem(instance), unexpected: false };
|
|
113
|
+
}
|
|
114
|
+
if (error instanceof import_absences.UnknownAbsenceTypeError) {
|
|
115
|
+
return { problem: new HttpProblem(400, "bad_request", error.message).toProblem(instance), unexpected: false };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
problem: new HttpProblem(500, "internal_error", "the request could not be processed").toProblem(instance),
|
|
119
|
+
unexpected: true
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
123
|
+
0 && (module.exports = {
|
|
124
|
+
HttpProblem,
|
|
125
|
+
PROBLEM_CONTENT_TYPE,
|
|
126
|
+
problem,
|
|
127
|
+
toProblem
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=problem.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/web/problem.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * `application/problem+json` responses (RFC 7807) with stable machine readable codes.\n *\n * Expected failures are raised as `HttpProblem` by the route handlers. Errors from the domain and storage\n * layers are mapped here, so a client always receives the same shape:\n *\n * ```\n * { \"type\": \"about:blank\", \"title\": \"Not Found\", \"status\": 404, \"code\": \"not_found\", \"detail\": \"\u2026\" }\n * ```\n *\n * Messages of unknown errors are **not** exposed (they may contain internals); they are logged by the caller.\n */\n\nimport { RevisionConflictError } from \"../db/repositories/entries\";\nimport { LoginExistsError, UnknownRoleError } from \"../db/repositories/users\";\nimport { UnknownAbsenceTypeError } from \"../db/repositories/absences\";\nimport { NotFoundError, ValidationError, type FieldIssue } from \"../errors\";\n\n/** The content type every problem document is sent with (RFC 9457). */\nexport const PROBLEM_CONTENT_TYPE = \"application/problem+json; charset=utf-8\";\n\n/** Stable error codes of the API. */\nexport type ProblemCode =\n\t| \"bad_request\"\n\t| \"invalid_credentials\"\n\t| \"locked_out\"\n\t| \"no_session\"\n\t| \"session_expired\"\n\t| \"user_inactive\"\n\t| \"permission_denied\"\n\t| \"csrf_rejected\"\n\t| \"not_found\"\n\t| \"method_not_allowed\"\n\t| \"conflict\"\n\t| \"revision_conflict\"\n\t| \"payload_too_large\"\n\t| \"unsupported_media_type\"\n\t| \"kiosk_disabled\"\n\t| \"not_configured\"\n\t| \"report_font_missing\"\n\t| \"backup_invalid\"\n\t| \"import_mismatch\"\n\t| \"rate_limited\"\n\t// an employee may only change own punches inside the edit window of the instance\n\t| \"edit_window_closed\"\n\t| \"internal_error\";\n\n/** A problem document. */\nexport interface ProblemDetails {\n\t/** URI reference identifying the problem type (always `about:blank` here) */\n\ttype: string;\n\t/** Short, human readable summary */\n\ttitle: string;\n\t/** HTTP status code */\n\tstatus: number;\n\t/** Stable machine readable code */\n\tcode: ProblemCode;\n\t/** Explanation of this occurrence */\n\tdetail?: string;\n\t/** Request path the problem occurred on */\n\tinstance?: string;\n\t/** Invalid fields of a validation failure (RFC 9457) */\n\terrors?: { path: string; message: string }[];\n}\n\nconst TITLES: Record<number, string> = {\n\t400: \"Bad Request\",\n\t401: \"Unauthorized\",\n\t403: \"Forbidden\",\n\t404: \"Not Found\",\n\t405: \"Method Not Allowed\",\n\t409: \"Conflict\",\n\t413: \"Payload Too Large\",\n\t415: \"Unsupported Media Type\",\n\t423: \"Locked\",\n\t429: \"Too Many Requests\",\n\t500: \"Internal Server Error\",\n};\n\n/** An error that is returned to the client as `application/problem+json`. */\nexport class HttpProblem extends Error {\n\t/** Title used in the problem document. */\n\tpublic readonly problemTitle: string;\n\n\t/**\n\t * Creates the error.\n\t *\n\t * @param status - HTTP status code\n\t * @param code - stable error code\n\t * @param detail - optional explanation\n\t * @param title - optional title (defaults to the reason phrase of the status)\n\t * @param fields - optional list of invalid fields\n\t */\n\tconstructor(\n\t\tpublic readonly status: number,\n\t\tpublic readonly code: ProblemCode,\n\t\tpublic readonly detail?: string,\n\t\ttitle?: string,\n\t\tpublic readonly fields: FieldIssue[] = [],\n\t) {\n\t\tsuper(`${status} ${code}${detail ? `: ${detail}` : \"\"}`);\n\t\tthis.name = \"HttpProblem\";\n\t\tthis.problemTitle = title ?? TITLES[status] ?? \"Error\";\n\t}\n\n\t/**\n\t * Converts the error into a problem document.\n\t *\n\t * @param instance - request path the problem occurred on\n\t * @returns problem document\n\t */\n\tpublic toProblem(instance?: string): ProblemDetails {\n\t\treturn {\n\t\t\ttype: \"about:blank\",\n\t\t\ttitle: this.problemTitle,\n\t\t\tstatus: this.status,\n\t\t\tcode: this.code,\n\t\t\t...(this.detail ? { detail: this.detail } : {}),\n\t\t\t...(this.fields.length > 0\n\t\t\t\t? { errors: this.fields.map(field => ({ path: field.path, message: field.message })) }\n\t\t\t\t: {}),\n\t\t\t...(instance ? { instance } : {}),\n\t\t};\n\t}\n}\n\n/**\n * Short helper for the common cases.\n *\n * @param status - HTTP status code\n * @param code - stable error code\n * @param detail - optional explanation\n * @returns the problem error\n */\nexport function problem(status: number, code: ProblemCode, detail?: string): HttpProblem {\n\treturn new HttpProblem(status, code, detail);\n}\n\n/**\n * Maps an arbitrary error onto a problem document.\n *\n * Known domain errors keep their meaning (a stale revision is a `409`, a duplicate login a `409`); messages of\n * unknown errors are replaced by a generic text, so no internals leak into the response.\n *\n * @param error - error thrown by a handler or a service\n * @param instance - request path\n * @returns problem document and a flag telling whether this is an unexpected (server side) failure\n */\nexport function toProblem(error: unknown, instance?: string): { problem: ProblemDetails; unexpected: boolean } {\n\tif (error instanceof HttpProblem) {\n\t\treturn { problem: error.toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof ValidationError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(400, \"bad_request\", error.message, undefined, error.fields).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof NotFoundError) {\n\t\treturn { problem: new HttpProblem(404, \"not_found\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof RevisionConflictError) {\n\t\treturn {\n\t\t\tproblem: new HttpProblem(\n\t\t\t\t409,\n\t\t\t\t\"revision_conflict\",\n\t\t\t\t`the record was changed by someone else (current revision ${error.current.revision})`,\n\t\t\t).toProblem(instance),\n\t\t\tunexpected: false,\n\t\t};\n\t}\n\tif (error instanceof LoginExistsError || error instanceof UnknownRoleError) {\n\t\treturn { problem: new HttpProblem(409, \"conflict\", error.message).toProblem(instance), unexpected: false };\n\t}\n\tif (error instanceof UnknownAbsenceTypeError) {\n\t\treturn { problem: new HttpProblem(400, \"bad_request\", error.message).toProblem(instance), unexpected: false };\n\t}\n\n\treturn {\n\t\tproblem: new HttpProblem(500, \"internal_error\", \"the request could not be processed\").toProblem(instance),\n\t\tunexpected: true,\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAaA,qBAAsC;AACtC,mBAAmD;AACnD,sBAAwC;AACxC,oBAAgE;AAGzD,MAAM,uBAAuB;AA8CpC,MAAM,SAAiC;AAAA,EACtC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AACN;AAGO,MAAM,oBAAoB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAatC,YACiB,QACA,MACA,QAChB,OACgB,SAAuB,CAAC,GACvC;AAnGH;AAoGE,UAAM,GAAG,MAAM,IAAI,IAAI,GAAG,SAAS,KAAK,MAAM,KAAK,EAAE,EAAE;AANvC;AACA;AACA;AAEA;AAGhB,SAAK,OAAO;AACZ,SAAK,gBAAe,6BAAS,OAAO,MAAM,MAAtB,YAA2B;AAAA,EAChD;AAAA;AAAA,EArBgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA6BT,UAAU,UAAmC;AACnD,WAAO;AAAA,MACN,MAAM;AAAA,MACN,OAAO,KAAK;AAAA,MACZ,QAAQ,KAAK;AAAA,MACb,MAAM,KAAK;AAAA,MACX,GAAI,KAAK,SAAS,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;AAAA,MAC7C,GAAI,KAAK,OAAO,SAAS,IACtB,EAAE,QAAQ,KAAK,OAAO,IAAI,YAAU,EAAE,MAAM,MAAM,MAAM,SAAS,MAAM,QAAQ,EAAE,EAAE,IACnF,CAAC;AAAA,MACJ,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAChC;AAAA,EACD;AACD;AAUO,SAAS,QAAQ,QAAgB,MAAmB,QAA8B;AACxF,SAAO,IAAI,YAAY,QAAQ,MAAM,MAAM;AAC5C;AAYO,SAAS,UAAU,OAAgB,UAAqE;AAC9G,MAAI,iBAAiB,aAAa;AACjC,WAAO,EAAE,SAAS,MAAM,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAChE;AACA,MAAI,iBAAiB,+BAAiB;AACrC,WAAO;AAAA,MACN,SAAS,IAAI,YAAY,KAAK,eAAe,MAAM,SAAS,QAAW,MAAM,MAAM,EAAE,UAAU,QAAQ;AAAA,MACvG,YAAY;AAAA,IACb;AAAA,EACD;AACA,MAAI,iBAAiB,6BAAe;AACnC,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,aAAa,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC3G;AACA,MAAI,iBAAiB,sCAAuB;AAC3C,WAAO;AAAA,MACN,SAAS,IAAI;AAAA,QACZ;AAAA,QACA;AAAA,QACA,4DAA4D,MAAM,QAAQ,QAAQ;AAAA,MACnF,EAAE,UAAU,QAAQ;AAAA,MACpB,YAAY;AAAA,IACb;AAAA,EACD;AACA,MAAI,iBAAiB,iCAAoB,iBAAiB,+BAAkB;AAC3E,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,YAAY,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC1G;AACA,MAAI,iBAAiB,yCAAyB;AAC7C,WAAO,EAAE,SAAS,IAAI,YAAY,KAAK,eAAe,MAAM,OAAO,EAAE,UAAU,QAAQ,GAAG,YAAY,MAAM;AAAA,EAC7G;AAEA,SAAO;AAAA,IACN,SAAS,IAAI,YAAY,KAAK,kBAAkB,oCAAoC,EAAE,UAAU,QAAQ;AAAA,IACxG,YAAY;AAAA,EACb;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var rate_limit_exports = {};
|
|
20
|
+
__export(rate_limit_exports, {
|
|
21
|
+
createRateLimiter: () => createRateLimiter
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(rate_limit_exports);
|
|
24
|
+
function createRateLimiter() {
|
|
25
|
+
const hits = /* @__PURE__ */ new Map();
|
|
26
|
+
return {
|
|
27
|
+
check(options, key, now) {
|
|
28
|
+
var _a;
|
|
29
|
+
const timestamp = now != null ? now : Math.floor(Date.now() / 1e3);
|
|
30
|
+
const windowStart = timestamp - options.windowSeconds;
|
|
31
|
+
const id = `${options.name}:${key}`;
|
|
32
|
+
const recent = ((_a = hits.get(id)) != null ? _a : []).filter((at) => at > windowStart);
|
|
33
|
+
if (recent.length >= options.limit) {
|
|
34
|
+
const oldest = recent[0];
|
|
35
|
+
hits.set(id, recent);
|
|
36
|
+
return {
|
|
37
|
+
allowed: false,
|
|
38
|
+
remaining: 0,
|
|
39
|
+
retryAfterSeconds: Math.max(1, oldest + options.windowSeconds - timestamp)
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
recent.push(timestamp);
|
|
43
|
+
hits.set(id, recent);
|
|
44
|
+
if (hits.size > 1e3) {
|
|
45
|
+
for (const [candidate, stamps] of hits) {
|
|
46
|
+
if (stamps.every((at) => at <= windowStart)) {
|
|
47
|
+
hits.delete(candidate);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { allowed: true, remaining: options.limit - recent.length, retryAfterSeconds: 0 };
|
|
52
|
+
},
|
|
53
|
+
reset() {
|
|
54
|
+
hits.clear();
|
|
55
|
+
},
|
|
56
|
+
size() {
|
|
57
|
+
return hits.size;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
62
|
+
0 && (module.exports = {
|
|
63
|
+
createRateLimiter
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/lib/web/rate-limit.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Rate limiting for the API.\n *\n * A simple sliding window per key (route + client address) is enough here: the endpoints that matter are the\n * login, the punches, the synchronisation, the terminal and the RFID scan, and all of them are cheap to count.\n * The limiter lives in memory \u2014 a restart clears the counters, which is acceptable for a single instance and\n * avoids a table that would grow forever.\n *\n * The answer for a limited request is a `429` problem document with a `retry-after` header, so a client knows\n * when to try again.\n */\n\n/** A limit of one route class. */\nexport interface RateLimitOptions {\n\t/** Name of the class, used in the counter key (e.g. `login`) */\n\tname: string;\n\t/** Allowed requests inside the window */\n\tlimit: number;\n\t/** Length of the window in seconds */\n\twindowSeconds: number;\n}\n\n/** Result of a check. */\nexport interface RateLimitResult {\n\t/** False when the caller has to wait */\n\tallowed: boolean;\n\t/** Requests left in the current window */\n\tremaining: number;\n\t/** Seconds until the oldest request of the window leaves it */\n\tretryAfterSeconds: number;\n}\n\n/** The limiter. */\nexport interface RateLimiter {\n\t/** Counts a request and reports whether it may pass */\n\tcheck(options: RateLimitOptions, key: string, now?: number): RateLimitResult;\n\t/** Removes all counters (for tests) */\n\treset(): void;\n\t/** Number of tracked keys (diagnostics) */\n\tsize(): number;\n}\n\n/**\n * Creates a rate limiter.\n *\n * @returns the limiter\n */\nexport function createRateLimiter(): RateLimiter {\n\t/** Timestamps of the requests per key, oldest first. */\n\tconst hits = new Map<string, number[]>();\n\n\treturn {\n\t\tcheck(options: RateLimitOptions, key: string, now?: number): RateLimitResult {\n\t\t\tconst timestamp = now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst windowStart = timestamp - options.windowSeconds;\n\t\t\tconst id = `${options.name}:${key}`;\n\n\t\t\t// everything older than the window is irrelevant (and would only waste memory)\n\t\t\tconst recent = (hits.get(id) ?? []).filter(at => at > windowStart);\n\t\t\tif (recent.length >= options.limit) {\n\t\t\t\tconst oldest = recent[0];\n\t\t\t\thits.set(id, recent);\n\t\t\t\treturn {\n\t\t\t\t\tallowed: false,\n\t\t\t\t\tremaining: 0,\n\t\t\t\t\tretryAfterSeconds: Math.max(1, oldest + options.windowSeconds - timestamp),\n\t\t\t\t};\n\t\t\t}\n\n\t\t\trecent.push(timestamp);\n\t\t\thits.set(id, recent);\n\t\t\t// a cheap guard against unbounded growth: prune the map when it gets large\n\t\t\tif (hits.size > 1000) {\n\t\t\t\tfor (const [candidate, stamps] of hits) {\n\t\t\t\t\tif (stamps.every(at => at <= windowStart)) {\n\t\t\t\t\t\thits.delete(candidate);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn { allowed: true, remaining: options.limit - recent.length, retryAfterSeconds: 0 };\n\t\t},\n\n\t\treset(): void {\n\t\t\thits.clear();\n\t\t},\n\n\t\tsize(): number {\n\t\t\treturn hits.size;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA+CO,SAAS,oBAAiC;AAEhD,QAAM,OAAO,oBAAI,IAAsB;AAEvC,SAAO;AAAA,IACN,MAAM,SAA2B,KAAa,KAA+B;AApD/E;AAqDG,YAAM,YAAY,oBAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,cAAc,YAAY,QAAQ;AACxC,YAAM,KAAK,GAAG,QAAQ,IAAI,IAAI,GAAG;AAGjC,YAAM,WAAU,UAAK,IAAI,EAAE,MAAX,YAAgB,CAAC,GAAG,OAAO,QAAM,KAAK,WAAW;AACjE,UAAI,OAAO,UAAU,QAAQ,OAAO;AACnC,cAAM,SAAS,OAAO,CAAC;AACvB,aAAK,IAAI,IAAI,MAAM;AACnB,eAAO;AAAA,UACN,SAAS;AAAA,UACT,WAAW;AAAA,UACX,mBAAmB,KAAK,IAAI,GAAG,SAAS,QAAQ,gBAAgB,SAAS;AAAA,QAC1E;AAAA,MACD;AAEA,aAAO,KAAK,SAAS;AACrB,WAAK,IAAI,IAAI,MAAM;AAEnB,UAAI,KAAK,OAAO,KAAM;AACrB,mBAAW,CAAC,WAAW,MAAM,KAAK,MAAM;AACvC,cAAI,OAAO,MAAM,QAAM,MAAM,WAAW,GAAG;AAC1C,iBAAK,OAAO,SAAS;AAAA,UACtB;AAAA,QACD;AAAA,MACD;AAEA,aAAO,EAAE,SAAS,MAAM,WAAW,QAAQ,QAAQ,OAAO,QAAQ,mBAAmB,EAAE;AAAA,IACxF;AAAA,IAEA,QAAc;AACb,WAAK,MAAM;AAAA,IACZ;AAAA,IAEA,OAAe;AACd,aAAO,KAAK;AAAA,IACb;AAAA,EACD;AACD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|