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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/admin/i18n/de.json +34 -0
  4. package/admin/i18n/en.json +1 -0
  5. package/admin/i18n/es.json +34 -0
  6. package/admin/i18n/fr.json +34 -0
  7. package/admin/i18n/it.json +34 -0
  8. package/admin/i18n/nl.json +34 -0
  9. package/admin/i18n/pl.json +34 -0
  10. package/admin/i18n/pt.json +34 -0
  11. package/admin/i18n/ru.json +34 -0
  12. package/admin/i18n/uk.json +34 -0
  13. package/admin/i18n/zh-cn.json +34 -0
  14. package/admin/jsonConfig.json +266 -0
  15. package/admin/time-tracker.png +0 -0
  16. package/build/lib/adapter/automation.js +100 -0
  17. package/build/lib/adapter/automation.js.map +7 -0
  18. package/build/lib/adapter/commands.js +160 -0
  19. package/build/lib/adapter/commands.js.map +7 -0
  20. package/build/lib/adapter/messages.js +207 -0
  21. package/build/lib/adapter/messages.js.map +7 -0
  22. package/build/lib/adapter/presence.js +129 -0
  23. package/build/lib/adapter/presence.js.map +7 -0
  24. package/build/lib/adapter/states.js +330 -0
  25. package/build/lib/adapter/states.js.map +7 -0
  26. package/build/lib/adapter/triggers.js +92 -0
  27. package/build/lib/adapter/triggers.js.map +7 -0
  28. package/build/lib/db/database.js +96 -0
  29. package/build/lib/db/database.js.map +7 -0
  30. package/build/lib/db/migrations.js +603 -0
  31. package/build/lib/db/migrations.js.map +7 -0
  32. package/build/lib/db/repositories/absences.js +432 -0
  33. package/build/lib/db/repositories/absences.js.map +7 -0
  34. package/build/lib/db/repositories/audit.js +90 -0
  35. package/build/lib/db/repositories/audit.js.map +7 -0
  36. package/build/lib/db/repositories/automations.js +326 -0
  37. package/build/lib/db/repositories/automations.js.map +7 -0
  38. package/build/lib/db/repositories/entries.js +335 -0
  39. package/build/lib/db/repositories/entries.js.map +7 -0
  40. package/build/lib/db/repositories/holidays.js +156 -0
  41. package/build/lib/db/repositories/holidays.js.map +7 -0
  42. package/build/lib/db/repositories/payouts.js +221 -0
  43. package/build/lib/db/repositories/payouts.js.map +7 -0
  44. package/build/lib/db/repositories/rfid.js +264 -0
  45. package/build/lib/db/repositories/rfid.js.map +7 -0
  46. package/build/lib/db/repositories/rules.js +356 -0
  47. package/build/lib/db/repositories/rules.js.map +7 -0
  48. package/build/lib/db/repositories/settings.js +91 -0
  49. package/build/lib/db/repositories/settings.js.map +7 -0
  50. package/build/lib/db/repositories/terminals.js +243 -0
  51. package/build/lib/db/repositories/terminals.js.map +7 -0
  52. package/build/lib/db/repositories/triggers.js +240 -0
  53. package/build/lib/db/repositories/triggers.js.map +7 -0
  54. package/build/lib/db/repositories/users.js +491 -0
  55. package/build/lib/db/repositories/users.js.map +7 -0
  56. package/build/lib/db/seed.js +180 -0
  57. package/build/lib/db/seed.js.map +7 -0
  58. package/build/lib/domain/avatar.js +63 -0
  59. package/build/lib/domain/avatar.js.map +7 -0
  60. package/build/lib/domain/breaks.js +70 -0
  61. package/build/lib/domain/breaks.js.map +7 -0
  62. package/build/lib/domain/calculation.js +150 -0
  63. package/build/lib/domain/calculation.js.map +7 -0
  64. package/build/lib/domain/holidays.js +113 -0
  65. package/build/lib/domain/holidays.js.map +7 -0
  66. package/build/lib/domain/punch.js +99 -0
  67. package/build/lib/domain/punch.js.map +7 -0
  68. package/build/lib/domain/target.js +89 -0
  69. package/build/lib/domain/target.js.map +7 -0
  70. package/build/lib/errors.js +56 -0
  71. package/build/lib/errors.js.map +7 -0
  72. package/build/lib/reports/labels.js +358 -0
  73. package/build/lib/reports/labels.js.map +7 -0
  74. package/build/lib/reports/pdf.js +308 -0
  75. package/build/lib/reports/pdf.js.map +7 -0
  76. package/build/lib/reports/types.js +32 -0
  77. package/build/lib/reports/types.js.map +7 -0
  78. package/build/lib/reports/xls.js +211 -0
  79. package/build/lib/reports/xls.js.map +7 -0
  80. package/build/lib/services/aggregation.js +495 -0
  81. package/build/lib/services/aggregation.js.map +7 -0
  82. package/build/lib/services/auth.js +381 -0
  83. package/build/lib/services/auth.js.map +7 -0
  84. package/build/lib/services/backup.js +372 -0
  85. package/build/lib/services/backup.js.map +7 -0
  86. package/build/lib/services/closing.js +106 -0
  87. package/build/lib/services/closing.js.map +7 -0
  88. package/build/lib/services/sessionSecret.js +100 -0
  89. package/build/lib/services/sessionSecret.js.map +7 -0
  90. package/build/lib/services/sync.js +278 -0
  91. package/build/lib/services/sync.js.map +7 -0
  92. package/build/lib/util/time.js +125 -0
  93. package/build/lib/util/time.js.map +7 -0
  94. package/build/lib/web/api.js +2589 -0
  95. package/build/lib/web/api.js.map +7 -0
  96. package/build/lib/web/cookies.js +80 -0
  97. package/build/lib/web/cookies.js.map +7 -0
  98. package/build/lib/web/events.js +50 -0
  99. package/build/lib/web/events.js.map +7 -0
  100. package/build/lib/web/pin-guard.js +86 -0
  101. package/build/lib/web/pin-guard.js.map +7 -0
  102. package/build/lib/web/problem.js +129 -0
  103. package/build/lib/web/problem.js.map +7 -0
  104. package/build/lib/web/rate-limit.js +65 -0
  105. package/build/lib/web/rate-limit.js.map +7 -0
  106. package/build/lib/web/router.js +274 -0
  107. package/build/lib/web/router.js.map +7 -0
  108. package/build/lib/web/server.js +189 -0
  109. package/build/lib/web/server.js.map +7 -0
  110. package/build/lib/web/static.js +151 -0
  111. package/build/lib/web/static.js.map +7 -0
  112. package/build/lib/web/stream.js +135 -0
  113. package/build/lib/web/stream.js.map +7 -0
  114. package/build/main.js +986 -0
  115. package/build/main.js.map +7 -0
  116. package/io-package.json +221 -0
  117. package/package.json +131 -0
  118. package/src-pwa/src/i18n/de.json +359 -0
  119. package/src-pwa/src/i18n/en.json +359 -0
  120. package/src-pwa/src/i18n/es.json +359 -0
  121. package/src-pwa/src/i18n/fr.json +359 -0
  122. package/src-pwa/src/i18n/index.ts +96 -0
  123. package/src-pwa/src/i18n/it.json +359 -0
  124. package/src-pwa/src/i18n/nl.json +359 -0
  125. package/src-pwa/src/i18n/pl.json +359 -0
  126. package/src-pwa/src/i18n/pt.json +359 -0
  127. package/src-pwa/src/i18n/ru.json +359 -0
  128. package/src-pwa/src/i18n/uk.json +359 -0
  129. package/src-pwa/src/i18n/zh-cn.json +359 -0
  130. package/www/assets/index-QI3wrEvW.js +119 -0
  131. package/www/assets/index-QI3wrEvW.js.map +1 -0
  132. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js +3 -0
  133. package/www/assets/workbox-window.prod.es5-Bd17z0YL.js.map +1 -0
  134. package/www/favicon.svg +16 -0
  135. package/www/icon-192.png +0 -0
  136. package/www/icon-512.png +0 -0
  137. package/www/index.html +38 -0
  138. package/www/manifest.webmanifest +1 -0
  139. package/www/person.png +0 -0
  140. package/www/sw.js +3 -0
  141. package/www/sw.js.map +1 -0
  142. package/www/workbox-9c191d2f.js +3 -0
  143. package/www/workbox-9c191d2f.js.map +1 -0
@@ -0,0 +1,491 @@
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 users_exports = {};
20
+ __export(users_exports, {
21
+ LoginExistsError: () => LoginExistsError,
22
+ UnknownRoleError: () => UnknownRoleError,
23
+ createUsersRepository: () => createUsersRepository,
24
+ mapUserRow: () => mapUserRow,
25
+ mapWorkProfileRow: () => mapWorkProfileRow
26
+ });
27
+ module.exports = __toCommonJS(users_exports);
28
+ var import_errors = require("../../errors");
29
+ var import_audit = require("./audit");
30
+ class LoginExistsError extends Error {
31
+ /**
32
+ * Creates the error.
33
+ *
34
+ * @param login - login name that is already taken
35
+ */
36
+ constructor(login) {
37
+ super(`login "${login}" already exists`);
38
+ this.login = login;
39
+ this.name = "LoginExistsError";
40
+ }
41
+ }
42
+ class UnknownRoleError extends Error {
43
+ /**
44
+ * Creates the error.
45
+ *
46
+ * @param roleKey - unknown role key
47
+ */
48
+ constructor(roleKey) {
49
+ super(`unknown role "${roleKey}"`);
50
+ this.roleKey = roleKey;
51
+ this.name = "UnknownRoleError";
52
+ }
53
+ }
54
+ function mapUserRow(row) {
55
+ return {
56
+ id: row.id,
57
+ login: row.login,
58
+ passwordHash: row.password_hash,
59
+ displayName: row.display_name,
60
+ email: row.email,
61
+ rfidCard: row.rfid_card,
62
+ isActive: row.is_active !== 0,
63
+ mustChangePw: row.must_change_pw !== 0,
64
+ locale: row.locale,
65
+ timezone: row.timezone,
66
+ pinHash: row.pin_hash,
67
+ avatar: row.avatar,
68
+ createdAt: row.created_at,
69
+ updatedAt: row.updated_at,
70
+ lastLoginAt: row.last_login_at
71
+ };
72
+ }
73
+ function mapWorkProfileRow(row) {
74
+ return {
75
+ userId: row.user_id,
76
+ percent: row.percent,
77
+ weeklyHours: row.weekly_hours,
78
+ workdays: row.workdays,
79
+ startDate: row.start_date,
80
+ endDate: row.end_date,
81
+ overtimeCarryover: row.overtime_carryover,
82
+ vorholzeitPerYear: row.vorholzeit_per_year,
83
+ vacationCarryover: row.vacation_carryover,
84
+ vacationPerYear: row.vacation_per_year,
85
+ overtimeModel: row.overtime_model,
86
+ holidayFlags: row.holiday_flags,
87
+ pausePaidMinutes: row.pause_paid_minutes
88
+ };
89
+ }
90
+ const USER_COLUMNS = `id, login, password_hash, display_name, email, rfid_card, is_active,
91
+ must_change_pw, locale, timezone, pin_hash, avatar, created_at, updated_at, last_login_at`;
92
+ const PROFILE_COLUMNS = `user_id, percent, weekly_hours, workdays, start_date, end_date, overtime_carryover,
93
+ vorholzeit_per_year, vacation_carryover, vacation_per_year, overtime_model, holiday_flags, pause_paid_minutes`;
94
+ const AUDITED_USER_FIELDS = [
95
+ "login",
96
+ "displayName",
97
+ "email",
98
+ "rfidCard",
99
+ "isActive",
100
+ "mustChangePw",
101
+ "locale",
102
+ "timezone"
103
+ ];
104
+ const AUDITED_PROFILE_FIELDS = [
105
+ "percent",
106
+ "weeklyHours",
107
+ "workdays",
108
+ "startDate",
109
+ "endDate",
110
+ "overtimeCarryover",
111
+ "vorholzeitPerYear",
112
+ "vacationCarryover",
113
+ "vacationPerYear",
114
+ "overtimeModel",
115
+ "holidayFlags",
116
+ "pausePaidMinutes"
117
+ ];
118
+ function createUsersRepository(db) {
119
+ const selectById = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE id = ?`);
120
+ const selectByLogin = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE login = ? COLLATE NOCASE`);
121
+ const selectByRfid = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE rfid_card = ? COLLATE NOCASE LIMIT 1`);
122
+ const setPinHash = db.prepare("UPDATE users SET pin_hash = ?, updated_at = ? WHERE id = ?");
123
+ const setAvatarData = db.prepare("UPDATE users SET avatar = ?, updated_at = ? WHERE id = ?");
124
+ const selectAll = db.prepare(`SELECT ${USER_COLUMNS} FROM users ORDER BY display_name COLLATE NOCASE, id`);
125
+ const selectActive = db.prepare(
126
+ `SELECT ${USER_COLUMNS} FROM users WHERE is_active = 1 ORDER BY display_name COLLATE NOCASE, id`
127
+ );
128
+ const insertUser = db.prepare(
129
+ `INSERT INTO users
130
+ (login, password_hash, display_name, email, rfid_card, is_active, must_change_pw,
131
+ locale, timezone, created_at, updated_at)
132
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
133
+ );
134
+ const updateUser = db.prepare(
135
+ `UPDATE users SET display_name = ?, email = ?, locale = ?, timezone = ?, rfid_card = ?,
136
+ is_active = ?, must_change_pw = ?, password_hash = ?, updated_at = ?
137
+ WHERE id = ?`
138
+ );
139
+ const updateLastLogin = db.prepare("UPDATE users SET last_login_at = ? WHERE id = ?");
140
+ const insertDefaultProfile = db.prepare("INSERT OR IGNORE INTO work_profiles (user_id) VALUES (?)");
141
+ const selectProfile = db.prepare(`SELECT ${PROFILE_COLUMNS} FROM work_profiles WHERE user_id = ?`);
142
+ const upsertProfile = db.prepare(
143
+ `INSERT INTO work_profiles (${PROFILE_COLUMNS})
144
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145
+ ON CONFLICT(user_id) DO UPDATE SET
146
+ percent = excluded.percent,
147
+ weekly_hours = excluded.weekly_hours,
148
+ workdays = excluded.workdays,
149
+ start_date = excluded.start_date,
150
+ end_date = excluded.end_date,
151
+ overtime_carryover = excluded.overtime_carryover,
152
+ vorholzeit_per_year = excluded.vorholzeit_per_year,
153
+ vacation_carryover = excluded.vacation_carryover,
154
+ vacation_per_year = excluded.vacation_per_year,
155
+ overtime_model = excluded.overtime_model,
156
+ holiday_flags = excluded.holiday_flags,
157
+ pause_paid_minutes = excluded.pause_paid_minutes`
158
+ );
159
+ const selectRoles = db.prepare(
160
+ `SELECT r.key FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = ? ORDER BY r.key`
161
+ );
162
+ const selectRoleId = db.prepare("SELECT id FROM roles WHERE key = ?");
163
+ const deleteRoles = db.prepare("DELETE FROM user_roles WHERE user_id = ?");
164
+ const insertRole = db.prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)");
165
+ const selectPermissions = db.prepare(
166
+ `SELECT DISTINCT p.key FROM user_roles ur
167
+ JOIN role_permissions rp ON rp.role_id = ur.role_id
168
+ JOIN permissions p ON p.id = rp.permission_id
169
+ WHERE ur.user_id = ? ORDER BY p.key`
170
+ );
171
+ const selectRoleCatalog = db.prepare(
172
+ `SELECT r.key AS key, r.name AS name, p.key AS permission
173
+ FROM roles r
174
+ LEFT JOIN role_permissions rp ON rp.role_id = r.id
175
+ LEFT JOIN permissions p ON p.id = rp.permission_id
176
+ ORDER BY r.key, p.key`
177
+ );
178
+ const read = (id) => {
179
+ const row = selectById.get(id);
180
+ return row ? mapUserRow(row) : null;
181
+ };
182
+ const readProfile = (userId) => {
183
+ const row = selectProfile.get(userId);
184
+ return row ? mapWorkProfileRow(row) : null;
185
+ };
186
+ const defaultProfile = (userId) => ({
187
+ userId,
188
+ percent: 100,
189
+ weeklyHours: 42.5,
190
+ workdays: "0;1;1;1;1;1;0",
191
+ startDate: null,
192
+ endDate: null,
193
+ overtimeCarryover: 0,
194
+ vorholzeitPerYear: 0,
195
+ vacationCarryover: 0,
196
+ vacationPerYear: 0,
197
+ overtimeModel: "monthly",
198
+ holidayFlags: null,
199
+ pausePaidMinutes: 0
200
+ });
201
+ const resolveRoleIds = (roleKeys) => roleKeys.map((key) => {
202
+ const row = selectRoleId.get(key);
203
+ if (!row) {
204
+ throw new UnknownRoleError(key);
205
+ }
206
+ return row.id;
207
+ });
208
+ const writeRoles = (userId, roleIds) => {
209
+ deleteRoles.run(userId);
210
+ for (const roleId of roleIds) {
211
+ insertRole.run(userId, roleId);
212
+ }
213
+ };
214
+ return {
215
+ create(input) {
216
+ var _a, _b;
217
+ const login = input.login.trim();
218
+ if (!login) {
219
+ throw new import_errors.ValidationError("login must not be empty");
220
+ }
221
+ const existing = selectByLogin.get(login);
222
+ if (existing) {
223
+ throw new LoginExistsError(login);
224
+ }
225
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
226
+ const roleKeys = (_b = input.roleKeys) != null ? _b : [];
227
+ const roleIds = resolveRoleIds(roleKeys);
228
+ let userId = 0;
229
+ const run = db.transaction(() => {
230
+ var _a2, _b2, _c, _d, _e, _f, _g;
231
+ const result = insertUser.run(
232
+ login,
233
+ (_a2 = input.passwordHash) != null ? _a2 : "",
234
+ input.displayName,
235
+ (_b2 = input.email) != null ? _b2 : null,
236
+ (_c = input.rfidCard) != null ? _c : null,
237
+ 1,
238
+ input.mustChangePw === true ? 1 : 0,
239
+ (_d = input.locale) != null ? _d : "de-DE",
240
+ (_e = input.timezone) != null ? _e : "Europe/Berlin",
241
+ now,
242
+ now
243
+ );
244
+ userId = Number(result.lastInsertRowid);
245
+ insertDefaultProfile.run(userId);
246
+ writeRoles(userId, roleIds);
247
+ (0, import_audit.writeAuditLog)(db, {
248
+ atUtc: now,
249
+ actorId: (_f = input.actorId) != null ? _f : null,
250
+ action: "user.create",
251
+ entity: "user",
252
+ entityId: userId,
253
+ detail: { login, displayName: input.displayName, roles: roleKeys.slice().sort() },
254
+ ip: (_g = input.actorIp) != null ? _g : null
255
+ });
256
+ });
257
+ run();
258
+ const created = read(userId);
259
+ if (!created) {
260
+ throw new Error(`user ${userId} disappeared right after creation`);
261
+ }
262
+ return created;
263
+ },
264
+ findById: read,
265
+ findByLogin(login) {
266
+ const row = selectByLogin.get(login.trim());
267
+ return row ? mapUserRow(row) : null;
268
+ },
269
+ findByRfidCard(card) {
270
+ const row = selectByRfid.get(card.trim());
271
+ return row ? mapUserRow(row) : null;
272
+ },
273
+ setPin(input) {
274
+ var _a;
275
+ if (!read(input.userId)) {
276
+ throw new import_errors.NotFoundError(`user ${input.userId} not found`);
277
+ }
278
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
279
+ const run = db.transaction(() => {
280
+ var _a2;
281
+ setPinHash.run(input.pinHash, now, input.userId);
282
+ (0, import_audit.writeAuditLog)(db, {
283
+ atUtc: now,
284
+ actorId: input.actorId,
285
+ action: "user.pin",
286
+ entity: "user",
287
+ entityId: input.userId,
288
+ // the hash itself never reaches the audit trail
289
+ detail: { cleared: input.pinHash === null },
290
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
291
+ });
292
+ });
293
+ run();
294
+ },
295
+ setAvatar(input) {
296
+ var _a;
297
+ if (!read(input.userId)) {
298
+ throw new import_errors.NotFoundError(`user ${input.userId} not found`);
299
+ }
300
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
301
+ const run = db.transaction(() => {
302
+ var _a2;
303
+ setAvatarData.run(input.avatar, now, input.userId);
304
+ (0, import_audit.writeAuditLog)(db, {
305
+ atUtc: now,
306
+ actorId: input.actorId,
307
+ action: "user.avatar",
308
+ entity: "user",
309
+ entityId: input.userId,
310
+ // the picture itself never reaches the audit trail
311
+ detail: { cleared: input.avatar === null },
312
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
313
+ });
314
+ });
315
+ run();
316
+ },
317
+ list(options) {
318
+ const rows = ((options == null ? void 0 : options.includeInactive) === true ? selectAll : selectActive).all();
319
+ return rows.map(mapUserRow);
320
+ },
321
+ update(input) {
322
+ var _a, _b, _c, _d, _e, _f, _g;
323
+ const current = read(input.id);
324
+ if (!current) {
325
+ throw new import_errors.NotFoundError(`user ${input.id} not found`);
326
+ }
327
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
328
+ const next = {
329
+ ...current,
330
+ displayName: (_b = input.patch.displayName) != null ? _b : current.displayName,
331
+ email: input.patch.email === void 0 ? current.email : input.patch.email,
332
+ locale: (_c = input.patch.locale) != null ? _c : current.locale,
333
+ timezone: (_d = input.patch.timezone) != null ? _d : current.timezone,
334
+ rfidCard: input.patch.rfidCard === void 0 ? current.rfidCard : input.patch.rfidCard,
335
+ isActive: (_e = input.patch.isActive) != null ? _e : current.isActive,
336
+ mustChangePw: (_f = input.patch.mustChangePw) != null ? _f : current.mustChangePw,
337
+ passwordHash: (_g = input.patch.passwordHash) != null ? _g : current.passwordHash
338
+ };
339
+ const changes = (0, import_audit.diffFields)(
340
+ current,
341
+ next,
342
+ AUDITED_USER_FIELDS
343
+ );
344
+ const passwordChanged = next.passwordHash !== current.passwordHash;
345
+ if (Object.keys(changes).length === 0 && !passwordChanged) {
346
+ return current;
347
+ }
348
+ const action = !current.isActive && next.isActive ? "user.activate" : current.isActive && !next.isActive ? "user.deactivate" : "user.update";
349
+ const run = db.transaction(() => {
350
+ var _a2, _b2;
351
+ updateUser.run(
352
+ next.displayName,
353
+ next.email,
354
+ next.locale,
355
+ next.timezone,
356
+ next.rfidCard,
357
+ next.isActive ? 1 : 0,
358
+ next.mustChangePw ? 1 : 0,
359
+ next.passwordHash,
360
+ now,
361
+ input.id
362
+ );
363
+ (0, import_audit.writeAuditLog)(db, {
364
+ atUtc: now,
365
+ actorId: input.actorId,
366
+ action,
367
+ entity: "user",
368
+ entityId: input.id,
369
+ // never write the password hash into the audit trail
370
+ detail: { changes, passwordChanged, reason: (_a2 = input.reason) != null ? _a2 : null },
371
+ ip: (_b2 = input.actorIp) != null ? _b2 : null
372
+ });
373
+ });
374
+ run();
375
+ const updated = read(input.id);
376
+ if (!updated) {
377
+ throw new Error(`user ${input.id} disappeared right after the update`);
378
+ }
379
+ return updated;
380
+ },
381
+ roles(userId) {
382
+ return selectRoles.all(userId).map((row) => row.key);
383
+ },
384
+ roleCatalog() {
385
+ var _a;
386
+ const rows = selectRoleCatalog.all();
387
+ const catalog = /* @__PURE__ */ new Map();
388
+ for (const row of rows) {
389
+ const entry = (_a = catalog.get(row.key)) != null ? _a : { key: row.key, name: row.name, permissions: [] };
390
+ if (row.permission) {
391
+ entry.permissions.push(row.permission);
392
+ }
393
+ catalog.set(row.key, entry);
394
+ }
395
+ return [...catalog.values()];
396
+ },
397
+ setRoles(input) {
398
+ var _a;
399
+ if (!read(input.userId)) {
400
+ throw new import_errors.NotFoundError(`user ${input.userId} not found`);
401
+ }
402
+ const roleIds = resolveRoleIds(input.roleKeys);
403
+ const before = this.roles(input.userId);
404
+ const after = input.roleKeys.slice().sort();
405
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
406
+ const run = db.transaction(() => {
407
+ var _a2;
408
+ writeRoles(input.userId, roleIds);
409
+ (0, import_audit.writeAuditLog)(db, {
410
+ atUtc: now,
411
+ actorId: input.actorId,
412
+ action: "user.roles",
413
+ entity: "user",
414
+ entityId: input.userId,
415
+ detail: { old: before, new: after },
416
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
417
+ });
418
+ });
419
+ run();
420
+ return this.roles(input.userId);
421
+ },
422
+ permissions(userId) {
423
+ return selectPermissions.all(userId).map((row) => row.key);
424
+ },
425
+ hasPermissions(userId, required) {
426
+ const granted = new Set(this.permissions(userId));
427
+ return required.every((permission) => granted.has(permission));
428
+ },
429
+ getWorkProfile: readProfile,
430
+ saveWorkProfile(input) {
431
+ var _a;
432
+ const current = readProfile(input.userId);
433
+ const base = current != null ? current : defaultProfile(input.userId);
434
+ const next = { ...base, ...input.profile, userId: input.userId };
435
+ const changes = (0, import_audit.diffFields)(
436
+ base,
437
+ next,
438
+ AUDITED_PROFILE_FIELDS
439
+ );
440
+ if (current && Object.keys(changes).length === 0) {
441
+ return current;
442
+ }
443
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
444
+ const run = db.transaction(() => {
445
+ var _a2;
446
+ upsertProfile.run(
447
+ next.userId,
448
+ next.percent,
449
+ next.weeklyHours,
450
+ next.workdays,
451
+ next.startDate,
452
+ next.endDate,
453
+ next.overtimeCarryover,
454
+ next.vorholzeitPerYear,
455
+ next.vacationCarryover,
456
+ next.vacationPerYear,
457
+ next.overtimeModel,
458
+ next.holidayFlags,
459
+ next.pausePaidMinutes
460
+ );
461
+ (0, import_audit.writeAuditLog)(db, {
462
+ atUtc: now,
463
+ actorId: input.actorId,
464
+ action: "user.profile",
465
+ entity: "work_profile",
466
+ entityId: input.userId,
467
+ detail: { created: current === null, changes },
468
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
469
+ });
470
+ });
471
+ run();
472
+ const saved = readProfile(input.userId);
473
+ if (!saved) {
474
+ throw new Error(`work profile of user ${input.userId} disappeared right after the update`);
475
+ }
476
+ return saved;
477
+ },
478
+ recordLogin(userId, now) {
479
+ updateLastLogin.run(now != null ? now : Math.floor(Date.now() / 1e3), userId);
480
+ }
481
+ };
482
+ }
483
+ // Annotate the CommonJS export names for ESM import in node:
484
+ 0 && (module.exports = {
485
+ LoginExistsError,
486
+ UnknownRoleError,
487
+ createUsersRepository,
488
+ mapUserRow,
489
+ mapWorkProfileRow
490
+ });
491
+ //# sourceMappingURL=users.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/db/repositories/users.ts"],
4
+ "sourcesContent": ["/**\n * User repository (`users`, `user_roles`, `work_profiles`).\n *\n * Every write records an audit entry in the same transaction; passwords and password hashes never end up\n * in the audit trail, only the fact that they were changed.\n */\n\nimport type { Db } from \"../database\";\nimport type { OvertimeModel } from \"../../domain/calculation\";\nimport type { WorkProfile } from \"../../domain/target\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { writeAuditLog, diffFields } from \"./audit\";\n\n/** A user as stored in the database. */\nexport interface UserRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Unique login name */\n\tlogin: string;\n\t/** Hash of the password (never leaves the server) */\n\tpasswordHash: string;\n\t/** Name shown in the UI */\n\tdisplayName: string;\n\t/** E-mail address */\n\temail: string | null;\n\t/** RFID card id */\n\trfidCard: string | null;\n\t/** False for deactivated accounts */\n\tisActive: boolean;\n\t/** True while the user has to set a new password */\n\tmustChangePw: boolean;\n\t/** Preferred language */\n\tlocale: string;\n\t/** IANA time zone of the user */\n\ttimezone: string;\n\t/** Hash of the personal kiosk PIN, `null` when none is set */\n\tpinHash: string | null;\n\t/** Picture of the employee as a data URL, `null` when none is stored */\n\tavatar: string | null;\n\t/** Instant of creation, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Instant of the last change, UTC epoch seconds */\n\tupdatedAt: number;\n\t/** Instant of the last successful login, `null` if never logged in */\n\tlastLoginAt: number | null;\n}\n\n/** Employment parameters of a user (`work_profiles`). */\nexport interface WorkProfileRecord extends WorkProfile {\n\t/** Owner of the profile */\n\tuserId: number;\n\t/** Employment level in percent */\n\tpercent: number;\n\t/** Contracted hours per week at 100 % */\n\tweeklyHours: number;\n\t/** Active working days, `0;1;\u2026` for Sunday\u2026Saturday */\n\tworkdays: string;\n\t/** First day of the employment (UTC epoch seconds) */\n\tstartDate: number | null;\n\t/** Last day of the employment (UTC epoch seconds) */\n\tendDate: number | null;\n\t/** Overtime taken over from the previous year, in minutes */\n\tovertimeCarryover: number;\n\t/** Annual \u201CVorholzeit\u201D in minutes */\n\tvorholzeitPerYear: number;\n\t/** Vacation days taken over from the previous year */\n\tvacationCarryover: number;\n\t/** Annual vacation entitlement in days */\n\tvacationPerYear: number;\n\t/** Overtime model used for the reports */\n\tovertimeModel: OvertimeModel;\n\t/** Country specific holiday flags (JSON), `null` = instance default */\n\tholidayFlags: string | null;\n\t/** Minutes of the break per day that are paid (0 = the break is not paid at all) */\n\tpausePaidMinutes: number;\n}\n\n/** Input for creating a user. */\nexport interface CreateUserInput {\n\t/** Unique login name */\n\tlogin: string;\n\t/** Shown name */\n\tdisplayName: string;\n\t/** Password hash (already hashed by the caller) */\n\tpasswordHash?: string;\n\t/** E-mail address */\n\temail?: string | null;\n\t/** Preferred language, default `de-DE` */\n\tlocale?: string;\n\t/** IANA time zone, default `Europe/Berlin` */\n\ttimezone?: string;\n\t/** RFID card id */\n\trfidCard?: string | null;\n\t/** Forces a password change on the next login */\n\tmustChangePw?: boolean;\n\t/** Roles to assign (all permissions are granted by the seed roles) */\n\troleKeys?: string[];\n\t/** Id of the user who is an additional employee of the new account (self service) */\n\tactorId?: number | null;\n\t/** Client IP address */\n\tactorIp?: string | null;\n\t/** Instant of creation, defaults to now */\n\tnow?: number;\n}\n\n/** Input for changing a user. */\nexport interface UpdateUserInput {\n\t/** Id of the changed user */\n\tid: number;\n\t/** Fields to change */\n\tpatch: {\n\t\tdisplayName?: string;\n\t\temail?: string | null;\n\t\tlocale?: string;\n\t\ttimezone?: string;\n\t\trfidCard?: string | null;\n\t\tisActive?: boolean;\n\t\tmustChangePw?: boolean;\n\t\t/** Password hash (already hashed by the caller) */\n\t\tpasswordHash?: string;\n\t};\n\t/** Reason of the change (recorded in the audit trail) */\n\treason?: string | null;\n\t/** Who performs the change */\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/** Thrown when a login name is already taken. */\nexport class LoginExistsError extends Error {\n\t/**\n\t * Creates the error.\n\t *\n\t * @param login - login name that is already taken\n\t */\n\tconstructor(public readonly login: string) {\n\t\tsuper(`login \"${login}\" already exists`);\n\t\tthis.name = \"LoginExistsError\";\n\t}\n}\n\n/** Thrown when a role key is not known. */\nexport class UnknownRoleError extends Error {\n\t/**\n\t * Creates the error.\n\t *\n\t * @param roleKey - unknown role key\n\t */\n\tconstructor(public readonly roleKey: string) {\n\t\tsuper(`unknown role \"${roleKey}\"`);\n\t\tthis.name = \"UnknownRoleError\";\n\t}\n}\n\n/** User storage operations. */\nexport interface UsersRepository {\n\t/** Creates a user including a default work profile */\n\tcreate(input: CreateUserInput): UserRecord;\n\t/** Reads a user by id */\n\tfindById(id: number): UserRecord | null;\n\t/** Reads a user by login (case insensitive) */\n\tfindByLogin(login: string): UserRecord | null;\n\t/** Reads a user by the RFID card of the badge terminal */\n\tfindByRfidCard(card: string): UserRecord | null;\n\t/** Stores or clears the personal kiosk PIN (never written to the audit trail) */\n\tsetPin(input: {\n\t\tuserId: number;\n\t\tpinHash: string | null;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): void;\n\t/** Stores or clears the picture of an employee (the image itself never reaches the audit trail) */\n\tsetAvatar(input: {\n\t\t/** Id of the employee */\n\t\tuserId: number;\n\t\t/** Data URL of the picture, `null` clears it */\n\t\tavatar: string | null;\n\t\t/** Who changes the picture */\n\t\tactorId: number;\n\t\t/** Client IP address of the actor */\n\t\tactorIp?: string | null;\n\t\t/** Instant of the change, defaults to now */\n\t\tnow?: number;\n\t}): void;\n\t/** All users, optionally including the deactivated ones */\n\tlist(options?: { includeInactive?: boolean }): UserRecord[];\n\t/** Changes a user and audits the changed fields */\n\tupdate(input: UpdateUserInput): UserRecord;\n\t/** Role keys of a user (sorted) */\n\troles(userId: number): string[];\n\t/** All roles with their permission keys, sorted by key */\n\troleCatalog(): { key: string; name: string; permissions: string[] }[];\n\t/** Replaces the roles of a user */\n\tsetRoles(input: {\n\t\tuserId: number;\n\t\troleKeys: string[];\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): string[];\n\t/** Effective permission keys of a user (sorted) */\n\tpermissions(userId: number): string[];\n\t/** Checks whether a user holds all given permissions */\n\thasPermissions(userId: number, required: string[]): boolean;\n\t/** Work profile of a user, `null` when none is stored */\n\tgetWorkProfile(userId: number): WorkProfileRecord | null;\n\t/** Creates or updates the work profile */\n\tsaveWorkProfile(input: {\n\t\tuserId: number;\n\t\tprofile: Partial<Omit<WorkProfileRecord, \"userId\">>;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): WorkProfileRecord;\n\t/** Stores the instant of the last successful login */\n\trecordLogin(userId: number, now?: number): void;\n}\n\ninterface UserRow {\n\tid: number;\n\tlogin: string;\n\tpassword_hash: string;\n\tdisplay_name: string;\n\temail: string | null;\n\trfid_card: string | null;\n\tis_active: number;\n\tmust_change_pw: number;\n\tlocale: string;\n\ttimezone: string;\n\tpin_hash: string | null;\n\tavatar: string | null;\n\tcreated_at: number;\n\tupdated_at: number;\n\tlast_login_at: number | null;\n}\n\ninterface WorkProfileRow {\n\tuser_id: number;\n\tpercent: number;\n\tweekly_hours: number;\n\tworkdays: string;\n\tstart_date: number | null;\n\tend_date: number | null;\n\tovertime_carryover: number;\n\tvorholzeit_per_year: number;\n\tvacation_carryover: number;\n\tvacation_per_year: number;\n\tovertime_model: OvertimeModel;\n\tholiday_flags: string | null;\n\tpause_paid_minutes: number;\n}\n\n/**\n * Maps a database row to a user record.\n *\n * @param row - raw database row\n * @returns user record\n */\nexport function mapUserRow(row: UserRow): UserRecord {\n\treturn {\n\t\tid: row.id,\n\t\tlogin: row.login,\n\t\tpasswordHash: row.password_hash,\n\t\tdisplayName: row.display_name,\n\t\temail: row.email,\n\t\trfidCard: row.rfid_card,\n\t\tisActive: row.is_active !== 0,\n\t\tmustChangePw: row.must_change_pw !== 0,\n\t\tlocale: row.locale,\n\t\ttimezone: row.timezone,\n\t\tpinHash: row.pin_hash,\n\t\tavatar: row.avatar,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t\tlastLoginAt: row.last_login_at,\n\t};\n}\n\n/**\n * Maps a database row to a work profile record.\n *\n * @param row - raw database row\n * @returns work profile record\n */\nexport function mapWorkProfileRow(row: WorkProfileRow): WorkProfileRecord {\n\treturn {\n\t\tuserId: row.user_id,\n\t\tpercent: row.percent,\n\t\tweeklyHours: row.weekly_hours,\n\t\tworkdays: row.workdays,\n\t\tstartDate: row.start_date,\n\t\tendDate: row.end_date,\n\t\tovertimeCarryover: row.overtime_carryover,\n\t\tvorholzeitPerYear: row.vorholzeit_per_year,\n\t\tvacationCarryover: row.vacation_carryover,\n\t\tvacationPerYear: row.vacation_per_year,\n\t\tovertimeModel: row.overtime_model,\n\t\tholidayFlags: row.holiday_flags,\n\t\tpausePaidMinutes: row.pause_paid_minutes,\n\t};\n}\n\nconst USER_COLUMNS = `id, login, password_hash, display_name, email, rfid_card, is_active,\n\\tmust_change_pw, locale, timezone, pin_hash, avatar, created_at, updated_at, last_login_at`;\n\nconst PROFILE_COLUMNS = `user_id, percent, weekly_hours, workdays, start_date, end_date, overtime_carryover,\n\\tvorholzeit_per_year, vacation_carryover, vacation_per_year, overtime_model, holiday_flags, pause_paid_minutes`;\n\n/** Field names of a user that are compared for the audit trail (the password is reported separately). */\nconst AUDITED_USER_FIELDS: (keyof UserRecord)[] = [\n\t\"login\",\n\t\"displayName\",\n\t\"email\",\n\t\"rfidCard\",\n\t\"isActive\",\n\t\"mustChangePw\",\n\t\"locale\",\n\t\"timezone\",\n];\n\n/** Field names of a work profile that are compared for the audit trail. */\n/** Field names of a work profile that are compared for the audit trail. */\nconst AUDITED_PROFILE_FIELDS: (keyof WorkProfileRecord)[] = [\n\t\"percent\",\n\t\"weeklyHours\",\n\t\"workdays\",\n\t\"startDate\",\n\t\"endDate\",\n\t\"overtimeCarryover\",\n\t\"vorholzeitPerYear\",\n\t\"vacationCarryover\",\n\t\"vacationPerYear\",\n\t\"overtimeModel\",\n\t\"holidayFlags\",\n\t\"pausePaidMinutes\",\n];\n\n/**\n * Creates the user repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createUsersRepository(db: Db): UsersRepository {\n\tconst selectById = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE id = ?`);\n\tconst selectByLogin = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE login = ? COLLATE NOCASE`);\n\tconst selectByRfid = db.prepare(`SELECT ${USER_COLUMNS} FROM users WHERE rfid_card = ? COLLATE NOCASE LIMIT 1`);\n\tconst setPinHash = db.prepare(\"UPDATE users SET pin_hash = ?, updated_at = ? WHERE id = ?\");\n\tconst setAvatarData = db.prepare(\"UPDATE users SET avatar = ?, updated_at = ? WHERE id = ?\");\n\tconst selectAll = db.prepare(`SELECT ${USER_COLUMNS} FROM users ORDER BY display_name COLLATE NOCASE, id`);\n\tconst selectActive = db.prepare(\n\t\t`SELECT ${USER_COLUMNS} FROM users WHERE is_active = 1 ORDER BY display_name COLLATE NOCASE, id`,\n\t);\n\tconst insertUser = db.prepare(\n\t\t`INSERT INTO users\n\t\t (login, password_hash, display_name, email, rfid_card, is_active, must_change_pw,\n\t\t locale, timezone, created_at, updated_at)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updateUser = db.prepare(\n\t\t`UPDATE users SET display_name = ?, email = ?, locale = ?, timezone = ?, rfid_card = ?,\n\t\t is_active = ?, must_change_pw = ?, password_hash = ?, updated_at = ?\n\t\t WHERE id = ?`,\n\t);\n\tconst updateLastLogin = db.prepare(\"UPDATE users SET last_login_at = ? WHERE id = ?\");\n\tconst insertDefaultProfile = db.prepare(\"INSERT OR IGNORE INTO work_profiles (user_id) VALUES (?)\");\n\tconst selectProfile = db.prepare(`SELECT ${PROFILE_COLUMNS} FROM work_profiles WHERE user_id = ?`);\n\tconst upsertProfile = db.prepare(\n\t\t`INSERT INTO work_profiles (${PROFILE_COLUMNS})\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n\t\t ON CONFLICT(user_id) DO UPDATE SET\n\t\t percent = excluded.percent,\n\t\t weekly_hours = excluded.weekly_hours,\n\t\t workdays = excluded.workdays,\n\t\t start_date = excluded.start_date,\n\t\t end_date = excluded.end_date,\n\t\t overtime_carryover = excluded.overtime_carryover,\n\t\t vorholzeit_per_year = excluded.vorholzeit_per_year,\n\t\t vacation_carryover = excluded.vacation_carryover,\n\t\t vacation_per_year = excluded.vacation_per_year,\n\t\t overtime_model = excluded.overtime_model,\n\t\t holiday_flags = excluded.holiday_flags,\n\t\t pause_paid_minutes = excluded.pause_paid_minutes`,\n\t);\n\tconst selectRoles = db.prepare(\n\t\t`SELECT r.key FROM user_roles ur JOIN roles r ON r.id = ur.role_id WHERE ur.user_id = ? ORDER BY r.key`,\n\t);\n\tconst selectRoleId = db.prepare(\"SELECT id FROM roles WHERE key = ?\");\n\tconst deleteRoles = db.prepare(\"DELETE FROM user_roles WHERE user_id = ?\");\n\tconst insertRole = db.prepare(\"INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)\");\n\tconst selectPermissions = db.prepare(\n\t\t`SELECT DISTINCT p.key FROM user_roles ur\n\t\t JOIN role_permissions rp ON rp.role_id = ur.role_id\n\t\t JOIN permissions p ON p.id = rp.permission_id\n\t\t WHERE ur.user_id = ? ORDER BY p.key`,\n\t);\n\tconst selectRoleCatalog = db.prepare(\n\t\t`SELECT r.key AS key, r.name AS name, p.key AS permission\n\t\t FROM roles r\n\t\t LEFT JOIN role_permissions rp ON rp.role_id = r.id\n\t\t LEFT JOIN permissions p ON p.id = rp.permission_id\n\t\t ORDER BY r.key, p.key`,\n\t);\n\n\tconst read = (id: number): UserRecord | null => {\n\t\tconst row = selectById.get(id) as UserRow | undefined;\n\t\treturn row ? mapUserRow(row) : null;\n\t};\n\n\tconst readProfile = (userId: number): WorkProfileRecord | null => {\n\t\tconst row = selectProfile.get(userId) as WorkProfileRow | undefined;\n\t\treturn row ? mapWorkProfileRow(row) : null;\n\t};\n\n\t/**\n\t * Defaults of a fresh work profile (identical to the column defaults of the schema).\n\t *\n\t * @param userId - owner of the profile\n\t */\n\tconst defaultProfile = (userId: number): WorkProfileRecord => ({\n\t\tuserId,\n\t\tpercent: 100,\n\t\tweeklyHours: 42.5,\n\t\tworkdays: \"0;1;1;1;1;1;0\",\n\t\tstartDate: null,\n\t\tendDate: null,\n\t\tovertimeCarryover: 0,\n\t\tvorholzeitPerYear: 0,\n\t\tvacationCarryover: 0,\n\t\tvacationPerYear: 0,\n\t\tovertimeModel: \"monthly\",\n\t\tholidayFlags: null,\n\t\tpausePaidMinutes: 0,\n\t});\n\n\tconst resolveRoleIds = (roleKeys: string[]): number[] =>\n\t\troleKeys.map(key => {\n\t\t\tconst row = selectRoleId.get(key) as { id: number } | undefined;\n\t\t\tif (!row) {\n\t\t\t\tthrow new UnknownRoleError(key);\n\t\t\t}\n\t\t\treturn row.id;\n\t\t});\n\n\tconst writeRoles = (userId: number, roleIds: number[]): void => {\n\t\tdeleteRoles.run(userId);\n\t\tfor (const roleId of roleIds) {\n\t\t\tinsertRole.run(userId, roleId);\n\t\t}\n\t};\n\n\treturn {\n\t\tcreate(input: CreateUserInput): UserRecord {\n\t\t\tconst login = input.login.trim();\n\t\t\tif (!login) {\n\t\t\t\tthrow new ValidationError(\"login must not be empty\");\n\t\t\t}\n\t\t\tconst existing = selectByLogin.get(login) as UserRow | undefined;\n\t\t\tif (existing) {\n\t\t\t\tthrow new LoginExistsError(login);\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst roleKeys = input.roleKeys ?? [];\n\t\t\tconst roleIds = resolveRoleIds(roleKeys);\n\n\t\t\tlet userId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertUser.run(\n\t\t\t\t\tlogin,\n\t\t\t\t\tinput.passwordHash ?? \"\",\n\t\t\t\t\tinput.displayName,\n\t\t\t\t\tinput.email ?? null,\n\t\t\t\t\tinput.rfidCard ?? null,\n\t\t\t\t\t1,\n\t\t\t\t\tinput.mustChangePw === true ? 1 : 0,\n\t\t\t\t\tinput.locale ?? \"de-DE\",\n\t\t\t\t\tinput.timezone ?? \"Europe/Berlin\",\n\t\t\t\t\tnow,\n\t\t\t\t\tnow,\n\t\t\t\t);\n\t\t\t\tuserId = Number(result.lastInsertRowid);\n\t\t\t\tinsertDefaultProfile.run(userId);\n\t\t\t\twriteRoles(userId, roleIds);\n\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? null,\n\t\t\t\t\taction: \"user.create\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: userId,\n\t\t\t\t\tdetail: { login, displayName: input.displayName, roles: roleKeys.slice().sort() },\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 created = read(userId);\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`user ${userId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn created;\n\t\t},\n\n\t\tfindById: read,\n\n\t\tfindByLogin(login: string): UserRecord | null {\n\t\t\tconst row = selectByLogin.get(login.trim()) as UserRow | undefined;\n\t\t\treturn row ? mapUserRow(row) : null;\n\t\t},\n\n\t\tfindByRfidCard(card: string): UserRecord | null {\n\t\t\tconst row = selectByRfid.get(card.trim()) as UserRow | undefined;\n\t\t\treturn row ? mapUserRow(row) : null;\n\t\t},\n\n\t\tsetPin(input: {\n\t\t\tuserId: number;\n\t\t\tpinHash: string | null;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): void {\n\t\t\tif (!read(input.userId)) {\n\t\t\t\tthrow new NotFoundError(`user ${input.userId} not found`);\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\tsetPinHash.run(input.pinHash, now, input.userId);\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: \"user.pin\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: input.userId,\n\t\t\t\t\t// the hash itself never reaches the audit trail\n\t\t\t\t\tdetail: { cleared: input.pinHash === null },\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},\n\n\t\tsetAvatar(input: {\n\t\t\tuserId: number;\n\t\t\tavatar: string | null;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): void {\n\t\t\tif (!read(input.userId)) {\n\t\t\t\tthrow new NotFoundError(`user ${input.userId} not found`);\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\tsetAvatarData.run(input.avatar, now, input.userId);\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: \"user.avatar\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: input.userId,\n\t\t\t\t\t// the picture itself never reaches the audit trail\n\t\t\t\t\tdetail: { cleared: input.avatar === null },\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},\n\n\t\tlist(options?: { includeInactive?: boolean }): UserRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as UserRow[];\n\t\t\treturn rows.map(mapUserRow);\n\t\t},\n\n\t\tupdate(input: UpdateUserInput): UserRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`user ${input.id} not found`);\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst next: UserRecord = {\n\t\t\t\t...current,\n\t\t\t\tdisplayName: input.patch.displayName ?? current.displayName,\n\t\t\t\temail: input.patch.email === undefined ? current.email : input.patch.email,\n\t\t\t\tlocale: input.patch.locale ?? current.locale,\n\t\t\t\ttimezone: input.patch.timezone ?? current.timezone,\n\t\t\t\trfidCard: input.patch.rfidCard === undefined ? current.rfidCard : input.patch.rfidCard,\n\t\t\t\tisActive: input.patch.isActive ?? current.isActive,\n\t\t\t\tmustChangePw: input.patch.mustChangePw ?? current.mustChangePw,\n\t\t\t\tpasswordHash: input.patch.passwordHash ?? current.passwordHash,\n\t\t\t};\n\n\t\t\tconst changes = diffFields(\n\t\t\t\tcurrent as unknown as Record<string, unknown>,\n\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\tAUDITED_USER_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t);\n\t\t\tconst passwordChanged = next.passwordHash !== current.passwordHash;\n\t\t\tif (Object.keys(changes).length === 0 && !passwordChanged) {\n\t\t\t\t// nothing to do, keep `updated_at` untouched\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst action =\n\t\t\t\t!current.isActive && next.isActive\n\t\t\t\t\t? \"user.activate\"\n\t\t\t\t\t: current.isActive && !next.isActive\n\t\t\t\t\t\t? \"user.deactivate\"\n\t\t\t\t\t\t: \"user.update\";\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupdateUser.run(\n\t\t\t\t\tnext.displayName,\n\t\t\t\t\tnext.email,\n\t\t\t\t\tnext.locale,\n\t\t\t\t\tnext.timezone,\n\t\t\t\t\tnext.rfidCard,\n\t\t\t\t\tnext.isActive ? 1 : 0,\n\t\t\t\t\tnext.mustChangePw ? 1 : 0,\n\t\t\t\t\tnext.passwordHash,\n\t\t\t\t\tnow,\n\t\t\t\t\tinput.id,\n\t\t\t\t);\n\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,\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\t// never write the password hash into the audit trail\n\t\t\t\t\tdetail: { changes, passwordChanged, reason: input.reason ?? null },\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 updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`user ${input.id} disappeared right after the update`);\n\t\t\t}\n\t\t\treturn updated;\n\t\t},\n\n\t\troles(userId: number): string[] {\n\t\t\treturn (selectRoles.all(userId) as { key: string }[]).map(row => row.key);\n\t\t},\n\n\t\troleCatalog(): { key: string; name: string; permissions: string[] }[] {\n\t\t\tconst rows = selectRoleCatalog.all() as { key: string; name: string; permission: string | null }[];\n\t\t\tconst catalog = new Map<string, { key: string; name: string; permissions: string[] }>();\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst entry = catalog.get(row.key) ?? { key: row.key, name: row.name, permissions: [] };\n\t\t\t\tif (row.permission) {\n\t\t\t\t\tentry.permissions.push(row.permission);\n\t\t\t\t}\n\t\t\t\tcatalog.set(row.key, entry);\n\t\t\t}\n\t\t\treturn [...catalog.values()];\n\t\t},\n\n\t\tsetRoles(input: {\n\t\t\tuserId: number;\n\t\t\troleKeys: string[];\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): string[] {\n\t\t\tif (!read(input.userId)) {\n\t\t\t\tthrow new NotFoundError(`user ${input.userId} not found`);\n\t\t\t}\n\t\t\t// resolve (and validate) all keys before the first write\n\t\t\tconst roleIds = resolveRoleIds(input.roleKeys);\n\t\t\tconst before = this.roles(input.userId);\n\t\t\tconst after = input.roleKeys.slice().sort();\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\twriteRoles(input.userId, roleIds);\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: \"user.roles\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: input.userId,\n\t\t\t\t\tdetail: { old: before, new: after },\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 this.roles(input.userId);\n\t\t},\n\n\t\tpermissions(userId: number): string[] {\n\t\t\treturn (selectPermissions.all(userId) as { key: string }[]).map(row => row.key);\n\t\t},\n\n\t\thasPermissions(userId: number, required: string[]): boolean {\n\t\t\tconst granted = new Set(this.permissions(userId));\n\t\t\treturn required.every(permission => granted.has(permission));\n\t\t},\n\n\t\tgetWorkProfile: readProfile,\n\n\t\tsaveWorkProfile(input: {\n\t\t\tuserId: number;\n\t\t\tprofile: Partial<Omit<WorkProfileRecord, \"userId\">>;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): WorkProfileRecord {\n\t\t\tconst current = readProfile(input.userId);\n\t\t\tconst base = current ?? defaultProfile(input.userId);\n\t\t\tconst next: WorkProfileRecord = { ...base, ...input.profile, userId: input.userId };\n\n\t\t\tconst changes = diffFields(\n\t\t\t\tbase as unknown as Record<string, unknown>,\n\t\t\t\tnext as unknown as Record<string, unknown>,\n\t\t\t\tAUDITED_PROFILE_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t);\n\t\t\tif (current && Object.keys(changes).length === 0) {\n\t\t\t\treturn current;\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tupsertProfile.run(\n\t\t\t\t\tnext.userId,\n\t\t\t\t\tnext.percent,\n\t\t\t\t\tnext.weeklyHours,\n\t\t\t\t\tnext.workdays,\n\t\t\t\t\tnext.startDate,\n\t\t\t\t\tnext.endDate,\n\t\t\t\t\tnext.overtimeCarryover,\n\t\t\t\t\tnext.vorholzeitPerYear,\n\t\t\t\t\tnext.vacationCarryover,\n\t\t\t\t\tnext.vacationPerYear,\n\t\t\t\t\tnext.overtimeModel,\n\t\t\t\t\tnext.holidayFlags,\n\t\t\t\t\tnext.pausePaidMinutes,\n\t\t\t\t);\n\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: \"user.profile\",\n\t\t\t\t\tentity: \"work_profile\",\n\t\t\t\t\tentityId: input.userId,\n\t\t\t\t\tdetail: { created: current === null, changes },\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 = readProfile(input.userId);\n\t\t\tif (!saved) {\n\t\t\t\tthrow new Error(`work profile of user ${input.userId} disappeared right after the update`);\n\t\t\t}\n\t\t\treturn saved;\n\t\t},\n\n\t\trecordLogin(userId: number, now?: number): void {\n\t\t\tupdateLastLogin.run(now ?? Math.floor(Date.now() / 1000), userId);\n\t\t},\n\t};\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,oBAA+C;AAC/C,mBAA0C;AAyHnC,MAAM,yBAAyB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,YAA4B,OAAe;AAC1C,UAAM,UAAU,KAAK,kBAAkB;AADZ;AAE3B,SAAK,OAAO;AAAA,EACb;AACD;AAGO,MAAM,yBAAyB,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAM3C,YAA4B,SAAiB;AAC5C,UAAM,iBAAiB,OAAO,GAAG;AADN;AAE3B,SAAK,OAAO;AAAA,EACb;AACD;AA2GO,SAAS,WAAW,KAA0B;AACpD,SAAO;AAAA,IACN,IAAI,IAAI;AAAA,IACR,OAAO,IAAI;AAAA,IACX,cAAc,IAAI;AAAA,IAClB,aAAa,IAAI;AAAA,IACjB,OAAO,IAAI;AAAA,IACX,UAAU,IAAI;AAAA,IACd,UAAU,IAAI,cAAc;AAAA,IAC5B,cAAc,IAAI,mBAAmB;AAAA,IACrC,QAAQ,IAAI;AAAA,IACZ,UAAU,IAAI;AAAA,IACd,SAAS,IAAI;AAAA,IACb,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,WAAW,IAAI;AAAA,IACf,aAAa,IAAI;AAAA,EAClB;AACD;AAQO,SAAS,kBAAkB,KAAwC;AACzE,SAAO;AAAA,IACN,QAAQ,IAAI;AAAA,IACZ,SAAS,IAAI;AAAA,IACb,aAAa,IAAI;AAAA,IACjB,UAAU,IAAI;AAAA,IACd,WAAW,IAAI;AAAA,IACf,SAAS,IAAI;AAAA,IACb,mBAAmB,IAAI;AAAA,IACvB,mBAAmB,IAAI;AAAA,IACvB,mBAAmB,IAAI;AAAA,IACvB,iBAAiB,IAAI;AAAA,IACrB,eAAe,IAAI;AAAA,IACnB,cAAc,IAAI;AAAA,IAClB,kBAAkB,IAAI;AAAA,EACvB;AACD;AAEA,MAAM,eAAe;AAAA;AAGrB,MAAM,kBAAkB;AAAA;AAIxB,MAAM,sBAA4C;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAIA,MAAM,yBAAsD;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD;AAQO,SAAS,sBAAsB,IAAyB;AAC9D,QAAM,aAAa,GAAG,QAAQ,UAAU,YAAY,0BAA0B;AAC9E,QAAM,gBAAgB,GAAG,QAAQ,UAAU,YAAY,4CAA4C;AACnG,QAAM,eAAe,GAAG,QAAQ,UAAU,YAAY,wDAAwD;AAC9G,QAAM,aAAa,GAAG,QAAQ,4DAA4D;AAC1F,QAAM,gBAAgB,GAAG,QAAQ,0DAA0D;AAC3F,QAAM,YAAY,GAAG,QAAQ,UAAU,YAAY,sDAAsD;AACzG,QAAM,eAAe,GAAG;AAAA,IACvB,UAAU,YAAY;AAAA,EACvB;AACA,QAAM,aAAa,GAAG;AAAA,IACrB;AAAA;AAAA;AAAA;AAAA,EAID;AACA,QAAM,aAAa,GAAG;AAAA,IACrB;AAAA;AAAA;AAAA,EAGD;AACA,QAAM,kBAAkB,GAAG,QAAQ,iDAAiD;AACpF,QAAM,uBAAuB,GAAG,QAAQ,0DAA0D;AAClG,QAAM,gBAAgB,GAAG,QAAQ,UAAU,eAAe,uCAAuC;AACjG,QAAM,gBAAgB,GAAG;AAAA,IACxB,8BAA8B,eAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAe9C;AACA,QAAM,cAAc,GAAG;AAAA,IACtB;AAAA,EACD;AACA,QAAM,eAAe,GAAG,QAAQ,oCAAoC;AACpE,QAAM,cAAc,GAAG,QAAQ,0CAA0C;AACzE,QAAM,aAAa,GAAG,QAAQ,yDAAyD;AACvF,QAAM,oBAAoB,GAAG;AAAA,IAC5B;AAAA;AAAA;AAAA;AAAA,EAID;AACA,QAAM,oBAAoB,GAAG;AAAA,IAC5B;AAAA;AAAA;AAAA;AAAA;AAAA,EAKD;AAEA,QAAM,OAAO,CAAC,OAAkC;AAC/C,UAAM,MAAM,WAAW,IAAI,EAAE;AAC7B,WAAO,MAAM,WAAW,GAAG,IAAI;AAAA,EAChC;AAEA,QAAM,cAAc,CAAC,WAA6C;AACjE,UAAM,MAAM,cAAc,IAAI,MAAM;AACpC,WAAO,MAAM,kBAAkB,GAAG,IAAI;AAAA,EACvC;AAOA,QAAM,iBAAiB,CAAC,YAAuC;AAAA,IAC9D;AAAA,IACA,SAAS;AAAA,IACT,aAAa;AAAA,IACb,UAAU;AAAA,IACV,WAAW;AAAA,IACX,SAAS;AAAA,IACT,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,eAAe;AAAA,IACf,cAAc;AAAA,IACd,kBAAkB;AAAA,EACnB;AAEA,QAAM,iBAAiB,CAAC,aACvB,SAAS,IAAI,SAAO;AACnB,UAAM,MAAM,aAAa,IAAI,GAAG;AAChC,QAAI,CAAC,KAAK;AACT,YAAM,IAAI,iBAAiB,GAAG;AAAA,IAC/B;AACA,WAAO,IAAI;AAAA,EACZ,CAAC;AAEF,QAAM,aAAa,CAAC,QAAgB,YAA4B;AAC/D,gBAAY,IAAI,MAAM;AACtB,eAAW,UAAU,SAAS;AAC7B,iBAAW,IAAI,QAAQ,MAAM;AAAA,IAC9B;AAAA,EACD;AAEA,SAAO;AAAA,IACN,OAAO,OAAoC;AAxc7C;AAycG,YAAM,QAAQ,MAAM,MAAM,KAAK;AAC/B,UAAI,CAAC,OAAO;AACX,cAAM,IAAI,8BAAgB,yBAAyB;AAAA,MACpD;AACA,YAAM,WAAW,cAAc,IAAI,KAAK;AACxC,UAAI,UAAU;AACb,cAAM,IAAI,iBAAiB,KAAK;AAAA,MACjC;AAEA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,YAAW,WAAM,aAAN,YAAkB,CAAC;AACpC,YAAM,UAAU,eAAe,QAAQ;AAEvC,UAAI,SAAS;AACb,YAAM,MAAM,GAAG,YAAY,MAAY;AAvd1C,YAAAA,KAAAC,KAAA;AAwdI,cAAM,SAAS,WAAW;AAAA,UACzB;AAAA,WACAD,MAAA,MAAM,iBAAN,OAAAA,MAAsB;AAAA,UACtB,MAAM;AAAA,WACNC,MAAA,MAAM,UAAN,OAAAA,MAAe;AAAA,WACf,WAAM,aAAN,YAAkB;AAAA,UAClB;AAAA,UACA,MAAM,iBAAiB,OAAO,IAAI;AAAA,WAClC,WAAM,WAAN,YAAgB;AAAA,WAChB,WAAM,aAAN,YAAkB;AAAA,UAClB;AAAA,UACA;AAAA,QACD;AACA,iBAAS,OAAO,OAAO,eAAe;AACtC,6BAAqB,IAAI,MAAM;AAC/B,mBAAW,QAAQ,OAAO;AAE1B,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,UAAS,WAAM,YAAN,YAAiB;AAAA,UAC1B,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU;AAAA,UACV,QAAQ,EAAE,OAAO,aAAa,MAAM,aAAa,OAAO,SAAS,MAAM,EAAE,KAAK,EAAE;AAAA,UAChF,KAAI,WAAM,YAAN,YAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,MAAM;AAC3B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,QAAQ,MAAM,mCAAmC;AAAA,MAClE;AACA,aAAO;AAAA,IACR;AAAA,IAEA,UAAU;AAAA,IAEV,YAAY,OAAkC;AAC7C,YAAM,MAAM,cAAc,IAAI,MAAM,KAAK,CAAC;AAC1C,aAAO,MAAM,WAAW,GAAG,IAAI;AAAA,IAChC;AAAA,IAEA,eAAe,MAAiC;AAC/C,YAAM,MAAM,aAAa,IAAI,KAAK,KAAK,CAAC;AACxC,aAAO,MAAM,WAAW,GAAG,IAAI;AAAA,IAChC;AAAA,IAEA,OAAO,OAME;AA9gBX;AA+gBG,UAAI,CAAC,KAAK,MAAM,MAAM,GAAG;AACxB,cAAM,IAAI,4BAAc,QAAQ,MAAM,MAAM,YAAY;AAAA,MACzD;AACA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AAnhB1C,YAAAD;AAohBI,mBAAW,IAAI,MAAM,SAAS,KAAK,MAAM,MAAM;AAC/C,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA;AAAA,UAEhB,QAAQ,EAAE,SAAS,MAAM,YAAY,KAAK;AAAA,UAC1C,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAAA,IACL;AAAA,IAEA,UAAU,OAMD;AAziBX;AA0iBG,UAAI,CAAC,KAAK,MAAM,MAAM,GAAG;AACxB,cAAM,IAAI,4BAAc,QAAQ,MAAM,MAAM,YAAY;AAAA,MACzD;AACA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AA9iB1C,YAAAA;AA+iBI,sBAAc,IAAI,MAAM,QAAQ,KAAK,MAAM,MAAM;AACjD,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA;AAAA,UAEhB,QAAQ,EAAE,SAAS,MAAM,WAAW,KAAK;AAAA,UACzC,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAAA,IACL;AAAA,IAEA,KAAK,SAAuD;AAC3D,YAAM,SAAQ,mCAAS,qBAAoB,OAAO,YAAY,cAAc,IAAI;AAChF,aAAO,KAAK,IAAI,UAAU;AAAA,IAC3B;AAAA,IAEA,OAAO,OAAoC;AAnkB7C;AAokBG,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,4BAAc,QAAQ,MAAM,EAAE,YAAY;AAAA,MACrD;AAEA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,OAAmB;AAAA,QACxB,GAAG;AAAA,QACH,cAAa,WAAM,MAAM,gBAAZ,YAA2B,QAAQ;AAAA,QAChD,OAAO,MAAM,MAAM,UAAU,SAAY,QAAQ,QAAQ,MAAM,MAAM;AAAA,QACrE,SAAQ,WAAM,MAAM,WAAZ,YAAsB,QAAQ;AAAA,QACtC,WAAU,WAAM,MAAM,aAAZ,YAAwB,QAAQ;AAAA,QAC1C,UAAU,MAAM,MAAM,aAAa,SAAY,QAAQ,WAAW,MAAM,MAAM;AAAA,QAC9E,WAAU,WAAM,MAAM,aAAZ,YAAwB,QAAQ;AAAA,QAC1C,eAAc,WAAM,MAAM,iBAAZ,YAA4B,QAAQ;AAAA,QAClD,eAAc,WAAM,MAAM,iBAAZ,YAA4B,QAAQ;AAAA,MACnD;AAEA,YAAM,cAAU;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACD;AACA,YAAM,kBAAkB,KAAK,iBAAiB,QAAQ;AACtD,UAAI,OAAO,KAAK,OAAO,EAAE,WAAW,KAAK,CAAC,iBAAiB;AAE1D,eAAO;AAAA,MACR;AAEA,YAAM,SACL,CAAC,QAAQ,YAAY,KAAK,WACvB,kBACA,QAAQ,YAAY,CAAC,KAAK,WACzB,oBACA;AAEL,YAAM,MAAM,GAAG,YAAY,MAAY;AAxmB1C,YAAAA,KAAAC;AAymBI,mBAAW;AAAA,UACV,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK,WAAW,IAAI;AAAA,UACpB,KAAK,eAAe,IAAI;AAAA,UACxB,KAAK;AAAA,UACL;AAAA,UACA,MAAM;AAAA,QACP;AAEA,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf;AAAA,UACA,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA;AAAA,UAEhB,QAAQ,EAAE,SAAS,iBAAiB,SAAQD,MAAA,MAAM,WAAN,OAAAA,MAAgB,KAAK;AAAA,UACjE,KAAIC,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,UAAU,KAAK,MAAM,EAAE;AAC7B,UAAI,CAAC,SAAS;AACb,cAAM,IAAI,MAAM,QAAQ,MAAM,EAAE,qCAAqC;AAAA,MACtE;AACA,aAAO;AAAA,IACR;AAAA,IAEA,MAAM,QAA0B;AAC/B,aAAQ,YAAY,IAAI,MAAM,EAAwB,IAAI,SAAO,IAAI,GAAG;AAAA,IACzE;AAAA,IAEA,cAAsE;AA9oBxE;AA+oBG,YAAM,OAAO,kBAAkB,IAAI;AACnC,YAAM,UAAU,oBAAI,IAAkE;AACtF,iBAAW,OAAO,MAAM;AACvB,cAAM,SAAQ,aAAQ,IAAI,IAAI,GAAG,MAAnB,YAAwB,EAAE,KAAK,IAAI,KAAK,MAAM,IAAI,MAAM,aAAa,CAAC,EAAE;AACtF,YAAI,IAAI,YAAY;AACnB,gBAAM,YAAY,KAAK,IAAI,UAAU;AAAA,QACtC;AACA,gBAAQ,IAAI,IAAI,KAAK,KAAK;AAAA,MAC3B;AACA,aAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AAAA,IAC5B;AAAA,IAEA,SAAS,OAMI;AAjqBf;AAkqBG,UAAI,CAAC,KAAK,MAAM,MAAM,GAAG;AACxB,cAAM,IAAI,4BAAc,QAAQ,MAAM,MAAM,YAAY;AAAA,MACzD;AAEA,YAAM,UAAU,eAAe,MAAM,QAAQ;AAC7C,YAAM,SAAS,KAAK,MAAM,MAAM,MAAM;AACtC,YAAM,QAAQ,MAAM,SAAS,MAAM,EAAE,KAAK;AAE1C,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AA3qB1C,YAAAD;AA4qBI,mBAAW,MAAM,QAAQ,OAAO;AAChC,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ,EAAE,KAAK,QAAQ,KAAK,MAAM;AAAA,UAClC,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,aAAO,KAAK,MAAM,MAAM,MAAM;AAAA,IAC/B;AAAA,IAEA,YAAY,QAA0B;AACrC,aAAQ,kBAAkB,IAAI,MAAM,EAAwB,IAAI,SAAO,IAAI,GAAG;AAAA,IAC/E;AAAA,IAEA,eAAe,QAAgB,UAA6B;AAC3D,YAAM,UAAU,IAAI,IAAI,KAAK,YAAY,MAAM,CAAC;AAChD,aAAO,SAAS,MAAM,gBAAc,QAAQ,IAAI,UAAU,CAAC;AAAA,IAC5D;AAAA,IAEA,gBAAgB;AAAA,IAEhB,gBAAgB,OAMM;AA7sBxB;AA8sBG,YAAM,UAAU,YAAY,MAAM,MAAM;AACxC,YAAM,OAAO,4BAAW,eAAe,MAAM,MAAM;AACnD,YAAM,OAA0B,EAAE,GAAG,MAAM,GAAG,MAAM,SAAS,QAAQ,MAAM,OAAO;AAElF,YAAM,cAAU;AAAA,QACf;AAAA,QACA;AAAA,QACA;AAAA,MACD;AACA,UAAI,WAAW,OAAO,KAAK,OAAO,EAAE,WAAW,GAAG;AACjD,eAAO;AAAA,MACR;AAEA,YAAM,OAAM,WAAM,QAAN,YAAa,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI;AACrD,YAAM,MAAM,GAAG,YAAY,MAAY;AA5tB1C,YAAAA;AA6tBI,sBAAc;AAAA,UACb,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,UACL,KAAK;AAAA,QACN;AAEA,wCAAc,IAAI;AAAA,UACjB,OAAO;AAAA,UACP,SAAS,MAAM;AAAA,UACf,QAAQ;AAAA,UACR,QAAQ;AAAA,UACR,UAAU,MAAM;AAAA,UAChB,QAAQ,EAAE,SAAS,YAAY,MAAM,QAAQ;AAAA,UAC7C,KAAIA,MAAA,MAAM,YAAN,OAAAA,MAAiB;AAAA,QACtB,CAAC;AAAA,MACF,CAAC;AACD,UAAI;AAEJ,YAAM,QAAQ,YAAY,MAAM,MAAM;AACtC,UAAI,CAAC,OAAO;AACX,cAAM,IAAI,MAAM,wBAAwB,MAAM,MAAM,qCAAqC;AAAA,MAC1F;AACA,aAAO;AAAA,IACR;AAAA,IAEA,YAAY,QAAgB,KAAoB;AAC/C,sBAAgB,IAAI,oBAAO,KAAK,MAAM,KAAK,IAAI,IAAI,GAAI,GAAG,MAAM;AAAA,IACjE;AAAA,EACD;AACD;",
6
+ "names": ["_a", "_b"]
7
+ }