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,243 @@
|
|
|
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 terminals_exports = {};
|
|
20
|
+
__export(terminals_exports, {
|
|
21
|
+
createTerminalsRepository: () => createTerminalsRepository,
|
|
22
|
+
hashToken: () => hashToken,
|
|
23
|
+
newToken: () => newToken
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(terminals_exports);
|
|
26
|
+
var import_node_crypto = require("node:crypto");
|
|
27
|
+
var import_errors = require("../../errors");
|
|
28
|
+
var import_audit = require("./audit");
|
|
29
|
+
function hashToken(token) {
|
|
30
|
+
return (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
|
|
31
|
+
}
|
|
32
|
+
function newToken() {
|
|
33
|
+
return (0, import_node_crypto.randomBytes)(32).toString("base64url");
|
|
34
|
+
}
|
|
35
|
+
const TERMINAL_COLUMNS = `id, name, location, pin_required, is_active, expires_at, last_seen_at,
|
|
36
|
+
session_expires_at, created_at`;
|
|
37
|
+
function mapTerminalRow(row) {
|
|
38
|
+
return {
|
|
39
|
+
id: row.id,
|
|
40
|
+
name: row.name,
|
|
41
|
+
location: row.location,
|
|
42
|
+
pinRequired: row.pin_required !== 0,
|
|
43
|
+
isActive: row.is_active !== 0,
|
|
44
|
+
expiresAt: row.expires_at,
|
|
45
|
+
lastSeenAt: row.last_seen_at,
|
|
46
|
+
sessionExpiresAt: row.session_expires_at,
|
|
47
|
+
createdAt: row.created_at
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function createTerminalsRepository(db) {
|
|
51
|
+
const insertTerminal = db.prepare(
|
|
52
|
+
`INSERT INTO kiosk_terminals (name, location, token_hash, pin_required, is_active, expires_at, created_at)
|
|
53
|
+
VALUES (?, ?, ?, ?, 1, ?, ?)`
|
|
54
|
+
);
|
|
55
|
+
const selectById = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE id = ?`);
|
|
56
|
+
const selectByToken = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE token_hash = ?`);
|
|
57
|
+
const selectBySession = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE session_hash = ?`);
|
|
58
|
+
const selectAll = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals ORDER BY name COLLATE NOCASE, id`);
|
|
59
|
+
const selectActive = db.prepare(
|
|
60
|
+
`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE is_active = 1 ORDER BY name COLLATE NOCASE, id`
|
|
61
|
+
);
|
|
62
|
+
const updateSession = db.prepare(
|
|
63
|
+
"UPDATE kiosk_terminals SET session_hash = ?, session_expires_at = ? WHERE id = ?"
|
|
64
|
+
);
|
|
65
|
+
const updateHeartbeat = db.prepare(
|
|
66
|
+
"UPDATE kiosk_terminals SET session_expires_at = ?, last_seen_at = ? WHERE id = ?"
|
|
67
|
+
);
|
|
68
|
+
const deactivate = db.prepare(
|
|
69
|
+
"UPDATE kiosk_terminals SET is_active = 0, session_hash = NULL, session_expires_at = NULL WHERE id = ?"
|
|
70
|
+
);
|
|
71
|
+
const selectUsers = db.prepare(
|
|
72
|
+
"SELECT user_id AS userId FROM terminal_users WHERE terminal_id = ? ORDER BY user_id"
|
|
73
|
+
);
|
|
74
|
+
const deleteUsers = db.prepare("DELETE FROM terminal_users WHERE terminal_id = ?");
|
|
75
|
+
const insertUser = db.prepare(
|
|
76
|
+
"INSERT OR IGNORE INTO terminal_users (terminal_id, user_id, created_at) VALUES (?, ?, ?)"
|
|
77
|
+
);
|
|
78
|
+
const withUsers = (record) => ({
|
|
79
|
+
...record,
|
|
80
|
+
userIds: selectUsers.all(record.id).map((row) => row.userId)
|
|
81
|
+
});
|
|
82
|
+
const read = (id) => {
|
|
83
|
+
const row = selectById.get(id);
|
|
84
|
+
return row ? withUsers(mapTerminalRow(row)) : null;
|
|
85
|
+
};
|
|
86
|
+
const usable = (record, now) => record.isActive && (record.expiresAt === null || record.expiresAt > now);
|
|
87
|
+
return {
|
|
88
|
+
create(input) {
|
|
89
|
+
var _a, _b, _c;
|
|
90
|
+
const name = input.name.trim();
|
|
91
|
+
if (!name) {
|
|
92
|
+
throw new import_errors.ValidationError("name is required");
|
|
93
|
+
}
|
|
94
|
+
const ttlDays = (_a = input.ttlDays) != null ? _a : null;
|
|
95
|
+
if (ttlDays !== null && (!Number.isInteger(ttlDays) || ttlDays <= 0)) {
|
|
96
|
+
throw new import_errors.ValidationError(`ttlDays must be a positive whole number (got ${input.ttlDays})`);
|
|
97
|
+
}
|
|
98
|
+
const now = (_b = input.now) != null ? _b : Math.floor(Date.now() / 1e3);
|
|
99
|
+
const deviceToken = newToken();
|
|
100
|
+
const expiresAt = ttlDays === null ? null : now + ttlDays * 86400;
|
|
101
|
+
const userIds = [...new Set((_c = input.userIds) != null ? _c : [])];
|
|
102
|
+
let terminalId = 0;
|
|
103
|
+
const run = db.transaction(() => {
|
|
104
|
+
var _a2, _b2, _c2, _d, _e;
|
|
105
|
+
const result = insertTerminal.run(
|
|
106
|
+
name,
|
|
107
|
+
(_a2 = input.location) != null ? _a2 : null,
|
|
108
|
+
hashToken(deviceToken),
|
|
109
|
+
((_b2 = input.pinRequired) != null ? _b2 : true) ? 1 : 0,
|
|
110
|
+
expiresAt,
|
|
111
|
+
now
|
|
112
|
+
);
|
|
113
|
+
terminalId = Number(result.lastInsertRowid);
|
|
114
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
115
|
+
atUtc: now,
|
|
116
|
+
actorId: input.actorId,
|
|
117
|
+
action: "terminal.create",
|
|
118
|
+
entity: "kiosk_terminal",
|
|
119
|
+
entityId: terminalId,
|
|
120
|
+
// the token itself is never part of the audit trail
|
|
121
|
+
detail: {
|
|
122
|
+
name,
|
|
123
|
+
location: (_c2 = input.location) != null ? _c2 : null,
|
|
124
|
+
pinRequired: (_d = input.pinRequired) != null ? _d : true,
|
|
125
|
+
expiresAt,
|
|
126
|
+
userIds
|
|
127
|
+
},
|
|
128
|
+
ip: (_e = input.actorIp) != null ? _e : null
|
|
129
|
+
});
|
|
130
|
+
for (const userId of userIds) {
|
|
131
|
+
insertUser.run(terminalId, userId, now);
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
run();
|
|
135
|
+
const terminal = read(terminalId);
|
|
136
|
+
if (!terminal) {
|
|
137
|
+
throw new Error(`terminal ${terminalId} disappeared right after creation`);
|
|
138
|
+
}
|
|
139
|
+
return { terminal, deviceToken };
|
|
140
|
+
},
|
|
141
|
+
list(options) {
|
|
142
|
+
const rows = ((options == null ? void 0 : options.includeInactive) === true ? selectAll : selectActive).all();
|
|
143
|
+
return rows.map((row) => withUsers(mapTerminalRow(row)));
|
|
144
|
+
},
|
|
145
|
+
findById: read,
|
|
146
|
+
findByToken(deviceToken, now) {
|
|
147
|
+
const row = selectByToken.get(hashToken(deviceToken));
|
|
148
|
+
if (!row) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const record = mapTerminalRow(row);
|
|
152
|
+
return usable(record, now != null ? now : Math.floor(Date.now() / 1e3)) ? withUsers(record) : null;
|
|
153
|
+
},
|
|
154
|
+
startSession(input) {
|
|
155
|
+
var _a;
|
|
156
|
+
const terminal = read(input.id);
|
|
157
|
+
if (!terminal) {
|
|
158
|
+
throw new import_errors.NotFoundError(`terminal ${input.id} not found`);
|
|
159
|
+
}
|
|
160
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
161
|
+
const expiresAt = now + input.ttlMinutes * 60;
|
|
162
|
+
const terminalSession = newToken();
|
|
163
|
+
updateSession.run(hashToken(terminalSession), expiresAt, input.id);
|
|
164
|
+
return { terminalSession, expiresAt };
|
|
165
|
+
},
|
|
166
|
+
findBySession(terminalSession, now) {
|
|
167
|
+
const row = selectBySession.get(hashToken(terminalSession));
|
|
168
|
+
if (!row) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const record = mapTerminalRow(row);
|
|
172
|
+
const at = now != null ? now : Math.floor(Date.now() / 1e3);
|
|
173
|
+
if (!usable(record, at) || record.sessionExpiresAt === null || record.sessionExpiresAt <= at) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return withUsers(record);
|
|
177
|
+
},
|
|
178
|
+
touch(input) {
|
|
179
|
+
var _a;
|
|
180
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
181
|
+
const expiresAt = now + input.ttlMinutes * 60;
|
|
182
|
+
updateHeartbeat.run(expiresAt, now, input.id);
|
|
183
|
+
return { expiresAt };
|
|
184
|
+
},
|
|
185
|
+
setUsers(input) {
|
|
186
|
+
var _a, _b;
|
|
187
|
+
const terminal = read(input.id);
|
|
188
|
+
if (!terminal) {
|
|
189
|
+
throw new import_errors.NotFoundError(`terminal ${input.id} not found`);
|
|
190
|
+
}
|
|
191
|
+
const userIds = [...new Set(input.userIds)];
|
|
192
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
193
|
+
const run = db.transaction(() => {
|
|
194
|
+
var _a2;
|
|
195
|
+
deleteUsers.run(input.id);
|
|
196
|
+
for (const userId of userIds) {
|
|
197
|
+
insertUser.run(input.id, userId, now);
|
|
198
|
+
}
|
|
199
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
200
|
+
atUtc: now,
|
|
201
|
+
actorId: input.actorId,
|
|
202
|
+
action: "terminal.users",
|
|
203
|
+
entity: "kiosk_terminal",
|
|
204
|
+
entityId: input.id,
|
|
205
|
+
detail: { name: terminal.name, userIds },
|
|
206
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
run();
|
|
210
|
+
return (_b = read(input.id)) != null ? _b : terminal;
|
|
211
|
+
},
|
|
212
|
+
revoke(input) {
|
|
213
|
+
var _a;
|
|
214
|
+
const terminal = read(input.id);
|
|
215
|
+
if (!terminal || !terminal.isActive) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
219
|
+
const run = db.transaction(() => {
|
|
220
|
+
var _a2;
|
|
221
|
+
deactivate.run(input.id);
|
|
222
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
223
|
+
atUtc: now,
|
|
224
|
+
actorId: input.actorId,
|
|
225
|
+
action: "terminal.revoke",
|
|
226
|
+
entity: "kiosk_terminal",
|
|
227
|
+
entityId: input.id,
|
|
228
|
+
detail: { name: terminal.name },
|
|
229
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
run();
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
238
|
+
0 && (module.exports = {
|
|
239
|
+
createTerminalsRepository,
|
|
240
|
+
hashToken,
|
|
241
|
+
newToken
|
|
242
|
+
});
|
|
243
|
+
//# sourceMappingURL=terminals.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/terminals.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Kiosk terminals (`kiosk_terminals`).\n *\n * A terminal authenticates with a **device token** that is handed out exactly once (only its SHA-256 hash is\n * stored) and that can be revoked and given an expiry. A device exchanges the token for a **short lived\n * session**, which is stored as a hash as well and is extended by the heartbeat of the device. The repository\n * never returns a token or a session \u2014 it only ever sees their hashes.\n */\n\nimport { createHash, randomBytes } from \"node:crypto\";\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A kiosk terminal as it is stored (without any secret). */\nexport interface TerminalRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Display name, e.g. `Werkstatt` */\n\tname: string;\n\t/** Optional location of the device */\n\tlocation: string | null;\n\t/** True when a badge has to be combined with the personal PIN */\n\tpinRequired: boolean;\n\t/** False for revoked devices */\n\tisActive: boolean;\n\t/** Instant the device token expires, `null` = never */\n\texpiresAt: number | null;\n\t/** Instant of the last heartbeat, `null` if the device never called */\n\tlastSeenAt: number | null;\n\t/** Instant the current session expires, `null` when there is none */\n\tsessionExpiresAt: number | null;\n\t/** Instant of creation, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Employees shown on this terminal; an empty list means \u201Call employees\u201D */\n\tuserIds: number[];\n}\n\n/** Terminal storage operations. */\nexport interface TerminalsRepository {\n\t/** Creates a terminal and returns its device token (the only time it is visible) */\n\tcreate(input: {\n\t\tname: string;\n\t\tlocation?: string | null;\n\t\tpinRequired?: boolean;\n\t\t/** Employees shown on the terminal; leave it out for \u201Call employees\u201D */\n\t\tuserIds?: number[];\n\t\t/** Days until the device token expires, `null` = never */\n\t\tttlDays?: number | null;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): { terminal: TerminalRecord; deviceToken: string };\n\t/** All terminals, optionally including the revoked ones */\n\tlist(options?: { includeInactive?: boolean }): TerminalRecord[];\n\t/** Reads a terminal by id */\n\tfindById(id: number): TerminalRecord | null;\n\t/** Resolves a device token (checks revocation and expiry) */\n\tfindByToken(deviceToken: string, now?: number): TerminalRecord | null;\n\t/** Starts a session for a device and returns the session token (replaces a running session) */\n\tstartSession(input: { id: number; ttlMinutes: number; now?: number }): {\n\t\tterminalSession: string;\n\t\texpiresAt: number;\n\t};\n\t/** Resolves a session token (checks revocation and expiry) */\n\tfindBySession(terminalSession: string, now?: number): TerminalRecord | null;\n\t/** Extends a session and records the heartbeat */\n\ttouch(input: { id: number; ttlMinutes: number; now?: number }): { expiresAt: number };\n\t/** Replaces the employees of a terminal (an empty list means \u201Call employees\u201D) */\n\tsetUsers(input: {\n\t\tid: number;\n\t\tuserIds: number[];\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): TerminalRecord;\n\t/** Deactivates a terminal and ends its session */\n\trevoke(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n}\n\n/**\n * Builds the hash of a token (device token or session).\n *\n * @param token - token in clear text\n * @returns hex encoded SHA-256 hash\n */\nexport function hashToken(token: string): string {\n\treturn createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\n/**\n * Creates a random token.\n *\n * @returns base64url encoded token\n */\nexport function newToken(): string {\n\treturn randomBytes(32).toString(\"base64url\");\n}\n\ninterface TerminalRow {\n\tid: number;\n\tname: string;\n\tlocation: string | null;\n\tpin_required: number;\n\tis_active: number;\n\texpires_at: number | null;\n\tlast_seen_at: number | null;\n\tsession_expires_at: number | null;\n\tcreated_at: number;\n}\n\nconst TERMINAL_COLUMNS = `id, name, location, pin_required, is_active, expires_at, last_seen_at,\n\tsession_expires_at, created_at`;\n\n/**\n * Maps a database row to a terminal record (without its employees \u2014 they live in `terminal_users`).\n *\n * @param row - raw database row\n * @returns terminal record\n */\nfunction mapTerminalRow(row: TerminalRow): Omit<TerminalRecord, \"userIds\"> {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlocation: row.location,\n\t\tpinRequired: row.pin_required !== 0,\n\t\tisActive: row.is_active !== 0,\n\t\texpiresAt: row.expires_at,\n\t\tlastSeenAt: row.last_seen_at,\n\t\tsessionExpiresAt: row.session_expires_at,\n\t\tcreatedAt: row.created_at,\n\t};\n}\n\n/**\n * Creates the terminals repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createTerminalsRepository(db: Db): TerminalsRepository {\n\tconst insertTerminal = db.prepare(\n\t\t`INSERT INTO kiosk_terminals (name, location, token_hash, pin_required, is_active, expires_at, created_at)\n\t\t VALUES (?, ?, ?, ?, 1, ?, ?)`,\n\t);\n\tconst selectById = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE id = ?`);\n\tconst selectByToken = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE token_hash = ?`);\n\tconst selectBySession = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE session_hash = ?`);\n\tconst selectAll = db.prepare(`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals ORDER BY name COLLATE NOCASE, id`);\n\tconst selectActive = db.prepare(\n\t\t`SELECT ${TERMINAL_COLUMNS} FROM kiosk_terminals WHERE is_active = 1 ORDER BY name COLLATE NOCASE, id`,\n\t);\n\tconst updateSession = db.prepare(\n\t\t\"UPDATE kiosk_terminals SET session_hash = ?, session_expires_at = ? WHERE id = ?\",\n\t);\n\tconst updateHeartbeat = db.prepare(\n\t\t\"UPDATE kiosk_terminals SET session_expires_at = ?, last_seen_at = ? WHERE id = ?\",\n\t);\n\tconst deactivate = db.prepare(\n\t\t\"UPDATE kiosk_terminals SET is_active = 0, session_hash = NULL, session_expires_at = NULL WHERE id = ?\",\n\t);\n\tconst selectUsers = db.prepare(\n\t\t\"SELECT user_id AS userId FROM terminal_users WHERE terminal_id = ? ORDER BY user_id\",\n\t);\n\tconst deleteUsers = db.prepare(\"DELETE FROM terminal_users WHERE terminal_id = ?\");\n\tconst insertUser = db.prepare(\n\t\t\"INSERT OR IGNORE INTO terminal_users (terminal_id, user_id, created_at) VALUES (?, ?, ?)\",\n\t);\n\n\t/**\n\t * Attaches the employees of a terminal.\n\t *\n\t * @param record - terminal without its employees\n\t * @returns the terminal with its employees (an empty list means \u201Call employees\u201D)\n\t */\n\tconst withUsers = (record: Omit<TerminalRecord, \"userIds\">): TerminalRecord => ({\n\t\t...record,\n\t\tuserIds: (selectUsers.all(record.id) as { userId: number }[]).map(row => row.userId),\n\t});\n\n\t/**\n\t * Reads a terminal.\n\t *\n\t * @param id - terminal id\n\t * @returns terminal or `null`\n\t */\n\tconst read = (id: number): TerminalRecord | null => {\n\t\tconst row = selectById.get(id) as TerminalRow | undefined;\n\t\treturn row ? withUsers(mapTerminalRow(row)) : null;\n\t};\n\n\t/**\n\t * Checks whether a device may still be used.\n\t *\n\t * @param record - terminal\n\t * @param now - instant\n\t * @returns true when the device is active and its token has not expired\n\t */\n\tconst usable = (record: Omit<TerminalRecord, \"userIds\">, now: number): boolean =>\n\t\trecord.isActive && (record.expiresAt === null || record.expiresAt > now);\n\n\treturn {\n\t\tcreate(input: {\n\t\t\tname: string;\n\t\t\tlocation?: string | null;\n\t\t\tpinRequired?: boolean;\n\t\t\tuserIds?: number[];\n\t\t\tttlDays?: number | null;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): { terminal: TerminalRecord; deviceToken: string } {\n\t\t\tconst name = input.name.trim();\n\t\t\tif (!name) {\n\t\t\t\tthrow new ValidationError(\"name is required\");\n\t\t\t}\n\t\t\tconst ttlDays = input.ttlDays ?? null;\n\t\t\tif (ttlDays !== null && (!Number.isInteger(ttlDays) || ttlDays <= 0)) {\n\t\t\t\tthrow new ValidationError(`ttlDays must be a positive whole number (got ${input.ttlDays})`);\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst deviceToken = newToken();\n\t\t\tconst expiresAt = ttlDays === null ? null : now + ttlDays * 86400;\n\t\t\tconst userIds = [...new Set(input.userIds ?? [])];\n\n\t\t\tlet terminalId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertTerminal.run(\n\t\t\t\t\tname,\n\t\t\t\t\tinput.location ?? null,\n\t\t\t\t\thashToken(deviceToken),\n\t\t\t\t\t(input.pinRequired ?? true) ? 1 : 0,\n\t\t\t\t\texpiresAt,\n\t\t\t\t\tnow,\n\t\t\t\t);\n\t\t\t\tterminalId = Number(result.lastInsertRowid);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"terminal.create\",\n\t\t\t\t\tentity: \"kiosk_terminal\",\n\t\t\t\t\tentityId: terminalId,\n\t\t\t\t\t// the token itself is never part of the audit trail\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tname,\n\t\t\t\t\t\tlocation: input.location ?? null,\n\t\t\t\t\t\tpinRequired: input.pinRequired ?? true,\n\t\t\t\t\t\texpiresAt,\n\t\t\t\t\t\tuserIds,\n\t\t\t\t\t},\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t\t// the employees of this terminal (none = all employees)\n\t\t\t\tfor (const userId of userIds) {\n\t\t\t\t\tinsertUser.run(terminalId, userId, now);\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst terminal = read(terminalId);\n\t\t\tif (!terminal) {\n\t\t\t\tthrow new Error(`terminal ${terminalId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn { terminal, deviceToken };\n\t\t},\n\n\t\tlist(options?: { includeInactive?: boolean }): TerminalRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as TerminalRow[];\n\t\t\treturn rows.map(row => withUsers(mapTerminalRow(row)));\n\t\t},\n\n\t\tfindById: read,\n\n\t\tfindByToken(deviceToken: string, now?: number): TerminalRecord | null {\n\t\t\tconst row = selectByToken.get(hashToken(deviceToken)) as TerminalRow | undefined;\n\t\t\tif (!row) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst record = mapTerminalRow(row);\n\t\t\treturn usable(record, now ?? Math.floor(Date.now() / 1000)) ? withUsers(record) : null;\n\t\t},\n\n\t\tstartSession(input: { id: number; ttlMinutes: number; now?: number }): {\n\t\t\tterminalSession: string;\n\t\t\texpiresAt: number;\n\t\t} {\n\t\t\tconst terminal = read(input.id);\n\t\t\tif (!terminal) {\n\t\t\t\tthrow new NotFoundError(`terminal ${input.id} not found`);\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst expiresAt = now + input.ttlMinutes * 60;\n\t\t\tconst terminalSession = newToken();\n\t\t\t// a new session replaces a running one, so a stolen session cannot be used in parallel\n\t\t\tupdateSession.run(hashToken(terminalSession), expiresAt, input.id);\n\t\t\treturn { terminalSession, expiresAt };\n\t\t},\n\n\t\tfindBySession(terminalSession: string, now?: number): TerminalRecord | null {\n\t\t\tconst row = selectBySession.get(hashToken(terminalSession)) as TerminalRow | undefined;\n\t\t\tif (!row) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst record = mapTerminalRow(row);\n\t\t\tconst at = now ?? Math.floor(Date.now() / 1000);\n\t\t\tif (!usable(record, at) || record.sessionExpiresAt === null || record.sessionExpiresAt <= at) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\treturn withUsers(record);\n\t\t},\n\n\t\ttouch(input: { id: number; ttlMinutes: number; now?: number }): { expiresAt: number } {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst expiresAt = now + input.ttlMinutes * 60;\n\t\t\tupdateHeartbeat.run(expiresAt, now, input.id);\n\t\t\treturn { expiresAt };\n\t\t},\n\n\t\tsetUsers(input: {\n\t\t\tid: number;\n\t\t\tuserIds: number[];\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): TerminalRecord {\n\t\t\tconst terminal = read(input.id);\n\t\t\tif (!terminal) {\n\t\t\t\tthrow new NotFoundError(`terminal ${input.id} not found`);\n\t\t\t}\n\t\t\tconst userIds = [...new Set(input.userIds)];\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteUsers.run(input.id);\n\t\t\t\tfor (const userId of userIds) {\n\t\t\t\t\tinsertUser.run(input.id, userId, now);\n\t\t\t\t}\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"terminal.users\",\n\t\t\t\t\tentity: \"kiosk_terminal\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { name: terminal.name, userIds },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\treturn read(input.id) ?? terminal;\n\t\t},\n\n\t\trevoke(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst terminal = read(input.id);\n\t\t\tif (!terminal || !terminal.isActive) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeactivate.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"terminal.revoke\",\n\t\t\t\t\tentity: \"kiosk_terminal\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { name: terminal.name },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a", "_b", "_c"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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 triggers_exports = {};
|
|
20
|
+
__export(triggers_exports, {
|
|
21
|
+
createTriggersRepository: () => createTriggersRepository,
|
|
22
|
+
mapTriggerRuleRow: () => mapTriggerRuleRow
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(triggers_exports);
|
|
25
|
+
var import_errors = require("../../errors");
|
|
26
|
+
var import_audit = require("./audit");
|
|
27
|
+
const TRIGGER_COLUMNS = "id, label, source_state, mode, condition, user_id, action, is_active, cooldown_sec, last_fired_at";
|
|
28
|
+
const AUDITED_TRIGGER_FIELDS = [
|
|
29
|
+
"label",
|
|
30
|
+
"sourceState",
|
|
31
|
+
"mode",
|
|
32
|
+
"condition",
|
|
33
|
+
"userId",
|
|
34
|
+
"action",
|
|
35
|
+
"isActive",
|
|
36
|
+
"cooldownSec"
|
|
37
|
+
];
|
|
38
|
+
const MODES = ["condition", "user"];
|
|
39
|
+
const ACTIONS = ["punch", "quickPunch", "present", "absent"];
|
|
40
|
+
function mapTriggerRuleRow(row) {
|
|
41
|
+
return {
|
|
42
|
+
id: row.id,
|
|
43
|
+
label: row.label,
|
|
44
|
+
sourceState: row.source_state,
|
|
45
|
+
mode: row.mode === "user" ? "user" : "condition",
|
|
46
|
+
condition: row.condition,
|
|
47
|
+
userId: row.user_id,
|
|
48
|
+
action: ACTIONS.includes(row.action) ? row.action : "punch",
|
|
49
|
+
isActive: row.is_active !== 0,
|
|
50
|
+
cooldownSec: row.cooldown_sec,
|
|
51
|
+
lastFiredAt: row.last_fired_at
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function requireSourceState(value) {
|
|
55
|
+
const state = (value != null ? value : "").trim();
|
|
56
|
+
if (!state) {
|
|
57
|
+
throw new import_errors.ValidationError("sourceState is required");
|
|
58
|
+
}
|
|
59
|
+
if (/\s/.test(state)) {
|
|
60
|
+
throw new import_errors.ValidationError(`sourceState must not contain whitespace (got "${value}")`);
|
|
61
|
+
}
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
function requireMode(value) {
|
|
65
|
+
if (value === void 0) {
|
|
66
|
+
return "condition";
|
|
67
|
+
}
|
|
68
|
+
if (!MODES.includes(value)) {
|
|
69
|
+
throw new import_errors.ValidationError(`mode must be one of ${MODES.join(", ")} (got ${value})`);
|
|
70
|
+
}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
function requireAction(value) {
|
|
74
|
+
if (value === void 0) {
|
|
75
|
+
return "punch";
|
|
76
|
+
}
|
|
77
|
+
if (!ACTIONS.includes(value)) {
|
|
78
|
+
throw new import_errors.ValidationError(`action must be one of ${ACTIONS.join(", ")} (got ${value})`);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
function requireCooldown(value) {
|
|
83
|
+
const cooldown = value != null ? value : 0;
|
|
84
|
+
if (!Number.isInteger(cooldown) || cooldown < 0 || cooldown > 86400) {
|
|
85
|
+
throw new import_errors.ValidationError(`cooldownSec must be a whole number of seconds between 0 and 86400 (got ${value})`);
|
|
86
|
+
}
|
|
87
|
+
return cooldown;
|
|
88
|
+
}
|
|
89
|
+
function createTriggersRepository(db) {
|
|
90
|
+
const selectById = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules WHERE id = ?`);
|
|
91
|
+
const selectAll = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules ORDER BY id`);
|
|
92
|
+
const selectActive = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules WHERE is_active = 1 ORDER BY id`);
|
|
93
|
+
const insertRule = db.prepare(
|
|
94
|
+
`INSERT INTO trigger_rules
|
|
95
|
+
(label, source_state, mode, condition, user_id, action, is_active, cooldown_sec, created_at, updated_at)
|
|
96
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
97
|
+
);
|
|
98
|
+
const updateRule = db.prepare(
|
|
99
|
+
`UPDATE trigger_rules
|
|
100
|
+
SET label = ?, source_state = ?, mode = ?, condition = ?, user_id = ?, action = ?, is_active = ?,
|
|
101
|
+
cooldown_sec = ?, updated_at = ?
|
|
102
|
+
WHERE id = ?`
|
|
103
|
+
);
|
|
104
|
+
const deleteRule = db.prepare("DELETE FROM trigger_rules WHERE id = ?");
|
|
105
|
+
const touchFired = db.prepare("UPDATE trigger_rules SET last_fired_at = ? WHERE id = ?");
|
|
106
|
+
const read = (id) => {
|
|
107
|
+
const row = selectById.get(id);
|
|
108
|
+
return row ? mapTriggerRuleRow(row) : null;
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
list(options) {
|
|
112
|
+
const rows = ((options == null ? void 0 : options.includeInactive) === true ? selectAll : selectActive).all();
|
|
113
|
+
return rows.map(mapTriggerRuleRow);
|
|
114
|
+
},
|
|
115
|
+
findById: read,
|
|
116
|
+
save(input) {
|
|
117
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
118
|
+
const sourceState = requireSourceState(input.sourceState);
|
|
119
|
+
const mode = requireMode(input.mode);
|
|
120
|
+
const action = requireAction(input.action);
|
|
121
|
+
const cooldownSec = requireCooldown(input.cooldownSec);
|
|
122
|
+
const label = ((_a = input.label) != null ? _a : "").trim() || null;
|
|
123
|
+
const isActive = input.isActive !== false;
|
|
124
|
+
const now = (_b = input.now) != null ? _b : Math.floor(Date.now() / 1e3);
|
|
125
|
+
const condition = mode === "condition" ? ((_c = input.condition) != null ? _c : "").trim() : null;
|
|
126
|
+
if (mode === "condition" && !condition) {
|
|
127
|
+
throw new import_errors.ValidationError("condition is required for mode condition");
|
|
128
|
+
}
|
|
129
|
+
const userId = mode === "condition" ? (_d = input.userId) != null ? _d : null : null;
|
|
130
|
+
if (mode === "condition" && (!Number.isInteger(userId) || (userId != null ? userId : 0) <= 0)) {
|
|
131
|
+
throw new import_errors.ValidationError(`userId must reference an existing employee (got ${input.userId})`);
|
|
132
|
+
}
|
|
133
|
+
const current = input.id === void 0 ? null : read(input.id);
|
|
134
|
+
if (input.id !== void 0 && !current) {
|
|
135
|
+
throw new import_errors.NotFoundError(`trigger rule ${input.id} not found`);
|
|
136
|
+
}
|
|
137
|
+
const next = {
|
|
138
|
+
id: (_e = current == null ? void 0 : current.id) != null ? _e : 0,
|
|
139
|
+
label,
|
|
140
|
+
sourceState,
|
|
141
|
+
mode,
|
|
142
|
+
condition,
|
|
143
|
+
userId,
|
|
144
|
+
action,
|
|
145
|
+
isActive,
|
|
146
|
+
cooldownSec,
|
|
147
|
+
lastFiredAt: (_f = current == null ? void 0 : current.lastFiredAt) != null ? _f : null
|
|
148
|
+
};
|
|
149
|
+
const changes = current ? (0, import_audit.diffFields)(
|
|
150
|
+
current,
|
|
151
|
+
next,
|
|
152
|
+
AUDITED_TRIGGER_FIELDS
|
|
153
|
+
) : null;
|
|
154
|
+
if (current && Object.keys(changes != null ? changes : {}).length === 0) {
|
|
155
|
+
return current;
|
|
156
|
+
}
|
|
157
|
+
let ruleId = (_g = current == null ? void 0 : current.id) != null ? _g : 0;
|
|
158
|
+
const run = db.transaction(() => {
|
|
159
|
+
var _a2;
|
|
160
|
+
if (current) {
|
|
161
|
+
updateRule.run(
|
|
162
|
+
label,
|
|
163
|
+
sourceState,
|
|
164
|
+
mode,
|
|
165
|
+
condition,
|
|
166
|
+
userId,
|
|
167
|
+
action,
|
|
168
|
+
isActive ? 1 : 0,
|
|
169
|
+
cooldownSec,
|
|
170
|
+
now,
|
|
171
|
+
current.id
|
|
172
|
+
);
|
|
173
|
+
} else {
|
|
174
|
+
ruleId = Number(
|
|
175
|
+
insertRule.run(
|
|
176
|
+
label,
|
|
177
|
+
sourceState,
|
|
178
|
+
mode,
|
|
179
|
+
condition,
|
|
180
|
+
userId,
|
|
181
|
+
action,
|
|
182
|
+
isActive ? 1 : 0,
|
|
183
|
+
cooldownSec,
|
|
184
|
+
now,
|
|
185
|
+
now
|
|
186
|
+
).lastInsertRowid
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
190
|
+
atUtc: now,
|
|
191
|
+
actorId: input.actorId,
|
|
192
|
+
action: current ? "trigger_rule.update" : "trigger_rule.create",
|
|
193
|
+
entity: "trigger_rule",
|
|
194
|
+
entityId: ruleId,
|
|
195
|
+
detail: current ? { changes } : { sourceState, mode, condition, userId, action, isActive, cooldownSec },
|
|
196
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
run();
|
|
200
|
+
const saved = read(ruleId);
|
|
201
|
+
if (!saved) {
|
|
202
|
+
throw new Error(`trigger rule ${ruleId} disappeared right after the write`);
|
|
203
|
+
}
|
|
204
|
+
return saved;
|
|
205
|
+
},
|
|
206
|
+
remove(input) {
|
|
207
|
+
var _a;
|
|
208
|
+
const current = read(input.id);
|
|
209
|
+
if (!current) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
|
|
213
|
+
const run = db.transaction(() => {
|
|
214
|
+
var _a2;
|
|
215
|
+
deleteRule.run(input.id);
|
|
216
|
+
(0, import_audit.writeAuditLog)(db, {
|
|
217
|
+
atUtc: now,
|
|
218
|
+
actorId: input.actorId,
|
|
219
|
+
action: "trigger_rule.delete",
|
|
220
|
+
entity: "trigger_rule",
|
|
221
|
+
entityId: input.id,
|
|
222
|
+
detail: { sourceState: current.sourceState, mode: current.mode, action: current.action },
|
|
223
|
+
ip: (_a2 = input.actorIp) != null ? _a2 : null
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
run();
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
markFired(input) {
|
|
230
|
+
var _a;
|
|
231
|
+
touchFired.run((_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3), input.id);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
236
|
+
0 && (module.exports = {
|
|
237
|
+
createTriggersRepository,
|
|
238
|
+
mapTriggerRuleRow
|
|
239
|
+
});
|
|
240
|
+
//# sourceMappingURL=triggers.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../../src/lib/db/repositories/triggers.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Trigger rules (`trigger_rules`).\n *\n * A rule turns a write on a state of another adapter into a punch or a presence change. That is the ioBroker way\n * of connecting a fingerprint reader, a button or a door contact: configure the rule once in the administration\n * and the adapter keeps watching the state \u2014 no script, no second app, no HTTP call.\n *\n * Mode `condition`: the rule fires when the value of `sourceState` equals `condition`, and the punch goes to\n * `userId`. Mode `user`: the value itself names the employee (user id, login or display name), which fits a\n * reader that reports *who* touched it instead of a plain button.\n *\n * `cooldownSec` and `lastFiredAt` keep a chatty reader in check: a rule that fired less than the configured\n * seconds ago ignores the next write.\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** How a rule decides which employee it fires for. */\nexport type TriggerMode = \"condition\" | \"user\";\n\n/** What a rule does when it fires. */\nexport type TriggerAction = \"punch\" | \"quickPunch\" | \"present\" | \"absent\";\n\n/** A trigger rule as it is stored. */\nexport interface TriggerRuleRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Free-form label, shown in the administration */\n\tlabel: string | null;\n\t/** State of another adapter that is watched */\n\tsourceState: string;\n\t/** How the employee is resolved */\n\tmode: TriggerMode;\n\t/** Value the state has to carry to fire the rule (mode `condition`) */\n\tcondition: string | null;\n\t/** Employee the rule fires for (mode `condition`) */\n\tuserId: number | null;\n\t/** What happens when the rule fires */\n\taction: TriggerAction;\n\t/** Inactive rules are ignored but keep their history */\n\tisActive: boolean;\n\t/** Seconds that have to pass between two fires, `0` = no limit */\n\tcooldownSec: number;\n\t/** Instant the rule fired last, `null` when it never fired */\n\tlastFiredAt: number | null;\n}\n\n/** Input for creating or changing a rule. */\nexport interface SaveTriggerRuleInput {\n\t/** Id of an existing rule, omitted when a new rule is created */\n\tid?: number;\n\t/** Free-form label */\n\tlabel?: string | null;\n\t/** State of another adapter that is watched */\n\tsourceState: string;\n\t/** How the employee is resolved, defaults to `condition` */\n\tmode?: TriggerMode;\n\t/** Value the state has to carry (mode `condition`) */\n\tcondition?: string | null;\n\t/** Employee the rule fires for (mode `condition`) */\n\tuserId?: number | null;\n\t/** What happens when the rule fires, defaults to `punch` */\n\taction?: TriggerAction;\n\t/** `false` disables the rule without deleting it */\n\tisActive?: boolean;\n\t/** Seconds that have to pass between two fires */\n\tcooldownSec?: number;\n\t/** Who changes the rule */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of the change, defaults to now */\n\tnow?: number;\n}\n\n/** Rule storage operations. */\nexport interface TriggersRepository {\n\t/** All rules, optionally including the disabled ones */\n\tlist(options?: { includeInactive?: boolean }): TriggerRuleRecord[];\n\t/** Reads one rule */\n\tfindById(id: number): TriggerRuleRecord | null;\n\t/** Creates or updates a rule */\n\tsave(input: SaveTriggerRuleInput): TriggerRuleRecord;\n\t/** Deletes a rule */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Records a fire (leaves `updatedAt` alone: the configuration did not change) */\n\tmarkFired(input: { id: number; now?: number }): void;\n}\n\n/** Raw database row of a rule. */\ninterface TriggerRuleRow {\n\tid: number;\n\tlabel: string | null;\n\tsource_state: string;\n\tmode: string;\n\tcondition: string | null;\n\tuser_id: number | null;\n\taction: string;\n\tis_active: number;\n\tcooldown_sec: number;\n\tlast_fired_at: number | null;\n}\n\nconst TRIGGER_COLUMNS =\n\t\"id, label, source_state, mode, condition, user_id, action, is_active, cooldown_sec, last_fired_at\";\n\n/** Field names of a rule that are compared for the audit trail. */\nconst AUDITED_TRIGGER_FIELDS: (keyof TriggerRuleRecord)[] = [\n\t\"label\",\n\t\"sourceState\",\n\t\"mode\",\n\t\"condition\",\n\t\"userId\",\n\t\"action\",\n\t\"isActive\",\n\t\"cooldownSec\",\n];\n\n/** Modes a rule may use. */\nconst MODES: TriggerMode[] = [\"condition\", \"user\"];\n\n/** Actions a rule may perform. */\nconst ACTIONS: TriggerAction[] = [\"punch\", \"quickPunch\", \"present\", \"absent\"];\n\n/**\n * Maps a database row to a rule record.\n *\n * @param row - raw database row\n * @returns rule record\n */\nexport function mapTriggerRuleRow(row: TriggerRuleRow): TriggerRuleRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlabel: row.label,\n\t\tsourceState: row.source_state,\n\t\tmode: row.mode === \"user\" ? \"user\" : \"condition\",\n\t\tcondition: row.condition,\n\t\tuserId: row.user_id,\n\t\taction: ACTIONS.includes(row.action as TriggerAction) ? (row.action as TriggerAction) : \"punch\",\n\t\tisActive: row.is_active !== 0,\n\t\tcooldownSec: row.cooldown_sec,\n\t\tlastFiredAt: row.last_fired_at,\n\t};\n}\n\n/**\n * Validates the state id a rule watches.\n *\n * @param value - state id from the request\n * @returns the trimmed state id\n */\nfunction requireSourceState(value: string): string {\n\tconst state = (value ?? \"\").trim();\n\tif (!state) {\n\t\tthrow new ValidationError(\"sourceState is required\");\n\t}\n\tif (/\\s/.test(state)) {\n\t\tthrow new ValidationError(`sourceState must not contain whitespace (got \"${value}\")`);\n\t}\n\treturn state;\n}\n\n/**\n * Validates a mode, falling back to `condition`.\n *\n * @param value - mode from the request\n * @returns the validated mode\n */\nfunction requireMode(value: string | undefined): TriggerMode {\n\tif (value === undefined) {\n\t\treturn \"condition\";\n\t}\n\tif (!MODES.includes(value as TriggerMode)) {\n\t\tthrow new ValidationError(`mode must be one of ${MODES.join(\", \")} (got ${value})`);\n\t}\n\treturn value as TriggerMode;\n}\n\n/**\n * Validates an action, falling back to `punch`.\n *\n * @param value - action from the request\n * @returns the validated action\n */\nfunction requireAction(value: string | undefined): TriggerAction {\n\tif (value === undefined) {\n\t\treturn \"punch\";\n\t}\n\tif (!ACTIONS.includes(value as TriggerAction)) {\n\t\tthrow new ValidationError(`action must be one of ${ACTIONS.join(\", \")} (got ${value})`);\n\t}\n\treturn value as TriggerAction;\n}\n\n/**\n * Validates the seconds that have to pass between two fires.\n *\n * @param value - cooldown from the request\n * @returns the validated cooldown\n */\nfunction requireCooldown(value: number | undefined): number {\n\tconst cooldown = value ?? 0;\n\tif (!Number.isInteger(cooldown) || cooldown < 0 || cooldown > 86400) {\n\t\tthrow new ValidationError(`cooldownSec must be a whole number of seconds between 0 and 86400 (got ${value})`);\n\t}\n\treturn cooldown;\n}\n\n/**\n * Creates the trigger rule repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createTriggersRepository(db: Db): TriggersRepository {\n\tconst selectById = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules WHERE id = ?`);\n\tconst selectAll = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules ORDER BY id`);\n\tconst selectActive = db.prepare(`SELECT ${TRIGGER_COLUMNS} FROM trigger_rules WHERE is_active = 1 ORDER BY id`);\n\tconst insertRule = db.prepare(\n\t\t`INSERT INTO trigger_rules\n\t\t (label, source_state, mode, condition, user_id, action, is_active, cooldown_sec, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateRule = db.prepare(\n\t\t`UPDATE trigger_rules\n\t\t SET label = ?, source_state = ?, mode = ?, condition = ?, user_id = ?, action = ?, is_active = ?,\n\t\t cooldown_sec = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst deleteRule = db.prepare(\"DELETE FROM trigger_rules WHERE id = ?\");\n\tconst touchFired = db.prepare(\"UPDATE trigger_rules SET last_fired_at = ? WHERE id = ?\");\n\n\t/**\n\t * Reads one rule.\n\t *\n\t * @param id - rule id\n\t * @returns the rule or `null`\n\t */\n\tconst read = (id: number): TriggerRuleRecord | null => {\n\t\tconst row = selectById.get(id) as TriggerRuleRow | undefined;\n\t\treturn row ? mapTriggerRuleRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(options?: { includeInactive?: boolean }): TriggerRuleRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as TriggerRuleRow[];\n\t\t\treturn rows.map(mapTriggerRuleRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tsave(input: SaveTriggerRuleInput): TriggerRuleRecord {\n\t\t\tconst sourceState = requireSourceState(input.sourceState);\n\t\t\tconst mode = requireMode(input.mode);\n\t\t\tconst action = requireAction(input.action);\n\t\t\tconst cooldownSec = requireCooldown(input.cooldownSec);\n\t\t\tconst label = (input.label ?? \"\").trim() || null;\n\t\t\tconst isActive = input.isActive !== false;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\t// a rule of mode `condition` needs both: the value that fires it and the employee it fires for\n\t\t\tconst condition = mode === \"condition\" ? (input.condition ?? \"\").trim() : null;\n\t\t\tif (mode === \"condition\" && !condition) {\n\t\t\t\tthrow new ValidationError(\"condition is required for mode condition\");\n\t\t\t}\n\t\t\tconst userId = mode === \"condition\" ? (input.userId ?? null) : null;\n\t\t\tif (mode === \"condition\" && (!Number.isInteger(userId) || (userId ?? 0) <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must reference an existing employee (got ${input.userId})`);\n\t\t\t}\n\n\t\t\tconst current = input.id === undefined ? null : read(input.id);\n\t\t\tif (input.id !== undefined && !current) {\n\t\t\t\tthrow new NotFoundError(`trigger rule ${input.id} not found`);\n\t\t\t}\n\n\t\t\tconst next: TriggerRuleRecord = {\n\t\t\t\tid: current?.id ?? 0,\n\t\t\t\tlabel,\n\t\t\t\tsourceState,\n\t\t\t\tmode,\n\t\t\t\tcondition,\n\t\t\t\tuserId,\n\t\t\t\taction,\n\t\t\t\tisActive,\n\t\t\t\tcooldownSec,\n\t\t\t\tlastFiredAt: current?.lastFiredAt ?? null,\n\t\t\t};\n\t\t\tconst changes = current\n\t\t\t\t? diffFields(\n\t\t\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\t\t\tAUDITED_TRIGGER_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t\t\t)\n\t\t\t\t: null;\n\t\t\tif (current && Object.keys(changes ?? {}).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tlet ruleId = current?.id ?? 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tif (current) {\n\t\t\t\t\tupdateRule.run(\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\tsourceState,\n\t\t\t\t\t\tmode,\n\t\t\t\t\t\tcondition,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\taction,\n\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\tcooldownSec,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t\tcurrent.id,\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\truleId = Number(\n\t\t\t\t\t\tinsertRule.run(\n\t\t\t\t\t\t\tlabel,\n\t\t\t\t\t\t\tsourceState,\n\t\t\t\t\t\t\tmode,\n\t\t\t\t\t\t\tcondition,\n\t\t\t\t\t\t\tuserId,\n\t\t\t\t\t\t\taction,\n\t\t\t\t\t\t\tisActive ? 1 : 0,\n\t\t\t\t\t\t\tcooldownSec,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t\tnow,\n\t\t\t\t\t\t).lastInsertRowid,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: current ? \"trigger_rule.update\" : \"trigger_rule.create\",\n\t\t\t\t\tentity: \"trigger_rule\",\n\t\t\t\t\tentityId: ruleId,\n\t\t\t\t\tdetail: current\n\t\t\t\t\t\t? { changes }\n\t\t\t\t\t\t: { sourceState, mode, condition, userId, action, isActive, cooldownSec },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\n\t\t\tconst saved = read(ruleId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`trigger rule ${ruleId} disappeared right after the write`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeleteRule.run(input.id);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\taction: \"trigger_rule.delete\",\n\t\t\t\t\tentity: \"trigger_rule\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { sourceState: current.sourceState, mode: current.mode, action: current.action },\n\t\t\t\t\tip: input.actorIp ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\treturn true;\n\t\t},\n\n\t\tmarkFired(input: { id: number; now?: number }): void {\n\t\t\ttouchFired.run(input.now ?? Math.floor(Date.now() / 1000), input.id);\n\t\t},\n\t};\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["_a"]
|
|
7
|
+
}
|