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,221 @@
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 payouts_exports = {};
20
+ __export(payouts_exports, {
21
+ createPayoutsRepository: () => createPayoutsRepository,
22
+ mapPayoutRow: () => mapPayoutRow
23
+ });
24
+ module.exports = __toCommonJS(payouts_exports);
25
+ var import_errors = require("../../errors");
26
+ var import_audit = require("./audit");
27
+ const COLUMNS = "id, user_id, year, month, minutes, amount, note, created_at, created_by";
28
+ const AUDITED_FIELDS = ["minutes", "amount", "note"];
29
+ function mapPayoutRow(row) {
30
+ return {
31
+ id: row.id,
32
+ userId: row.user_id,
33
+ year: row.year,
34
+ month: row.month,
35
+ minutes: row.minutes,
36
+ amount: row.amount,
37
+ note: row.note,
38
+ createdAt: row.created_at,
39
+ createdBy: row.created_by
40
+ };
41
+ }
42
+ function requireYear(year) {
43
+ if (!Number.isInteger(year) || year < 2e3 || year > 2100) {
44
+ throw new import_errors.ValidationError(`year must be a four digit year between 2000 and 2100 (got ${year})`);
45
+ }
46
+ return year;
47
+ }
48
+ function requireMonth(month) {
49
+ if (month === null) {
50
+ return null;
51
+ }
52
+ if (!Number.isInteger(month) || month < 1 || month > 12) {
53
+ throw new import_errors.ValidationError(`month must be between 1 and 12 or null (got ${month})`);
54
+ }
55
+ return month;
56
+ }
57
+ function requireMinutes(minutes) {
58
+ if (!Number.isInteger(minutes) || minutes === 0) {
59
+ throw new import_errors.ValidationError(`minutes must be a whole number and not 0 (got ${minutes})`);
60
+ }
61
+ return minutes;
62
+ }
63
+ function requireAmount(amount) {
64
+ if (amount === null) {
65
+ return null;
66
+ }
67
+ if (!Number.isFinite(amount)) {
68
+ throw new import_errors.ValidationError(`amount must be a number or null (got ${amount})`);
69
+ }
70
+ return amount;
71
+ }
72
+ function createPayoutsRepository(db) {
73
+ const selectById = db.prepare(`SELECT ${COLUMNS} FROM payouts WHERE id = ?`);
74
+ const selectByUser = db.prepare(
75
+ `SELECT ${COLUMNS} FROM payouts
76
+ WHERE user_id = ? AND (? IS NULL OR year = ?) AND (? IS NULL OR month = ?)
77
+ ORDER BY year DESC, COALESCE(month, 0) DESC, id DESC`
78
+ );
79
+ const selectSum = db.prepare(
80
+ `SELECT COALESCE(SUM(minutes), 0) AS minutes FROM payouts
81
+ WHERE user_id = ? AND year = ? AND (? IS NULL OR month = ?)`
82
+ );
83
+ const insertPayout = db.prepare(
84
+ `INSERT INTO payouts (user_id, year, month, minutes, amount, note, created_at, created_by)
85
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
86
+ );
87
+ const updatePayout = db.prepare("UPDATE payouts SET minutes = ?, amount = ?, note = ? WHERE id = ?");
88
+ const deletePayout = db.prepare("DELETE FROM payouts WHERE id = ?");
89
+ const read = (id) => {
90
+ const row = selectById.get(id);
91
+ return row ? mapPayoutRow(row) : null;
92
+ };
93
+ return {
94
+ list(userId, options) {
95
+ var _a, _b;
96
+ const year = (_a = options == null ? void 0 : options.year) != null ? _a : null;
97
+ const month = (_b = options == null ? void 0 : options.month) != null ? _b : null;
98
+ const rows = selectByUser.all(userId, year, year, month, month);
99
+ return rows.map(mapPayoutRow);
100
+ },
101
+ findById: read,
102
+ create(input) {
103
+ var _a, _b, _c;
104
+ const year = requireYear(input.year);
105
+ const month = requireMonth((_a = input.month) != null ? _a : null);
106
+ const minutes = requireMinutes(input.minutes);
107
+ const amount = requireAmount((_b = input.amount) != null ? _b : null);
108
+ const now = (_c = input.now) != null ? _c : Math.floor(Date.now() / 1e3);
109
+ let payoutId = 0;
110
+ const run = db.transaction(() => {
111
+ var _a2, _b2;
112
+ const result = insertPayout.run(
113
+ input.userId,
114
+ year,
115
+ month,
116
+ minutes,
117
+ amount,
118
+ (_a2 = input.note) != null ? _a2 : null,
119
+ now,
120
+ input.actorId
121
+ );
122
+ payoutId = Number(result.lastInsertRowid);
123
+ (0, import_audit.writeAuditLog)(db, {
124
+ atUtc: now,
125
+ actorId: input.actorId,
126
+ action: "payout.create",
127
+ entity: "payout",
128
+ entityId: payoutId,
129
+ detail: { userId: input.userId, year, month, minutes, amount },
130
+ ip: (_b2 = input.actorIp) != null ? _b2 : null
131
+ });
132
+ });
133
+ run();
134
+ const created = read(payoutId);
135
+ if (!created) {
136
+ throw new Error(`payout ${payoutId} disappeared right after creation`);
137
+ }
138
+ return created;
139
+ },
140
+ update(input) {
141
+ var _a;
142
+ const current = read(input.id);
143
+ if (!current) {
144
+ throw new import_errors.NotFoundError(`payout ${input.id} not found`);
145
+ }
146
+ const next = {
147
+ ...current,
148
+ minutes: input.patch.minutes === void 0 ? current.minutes : requireMinutes(input.patch.minutes),
149
+ amount: input.patch.amount === void 0 ? current.amount : requireAmount(input.patch.amount),
150
+ note: input.patch.note === void 0 ? current.note : input.patch.note
151
+ };
152
+ const changes = (0, import_audit.diffFields)(
153
+ current,
154
+ next,
155
+ AUDITED_FIELDS
156
+ );
157
+ if (Object.keys(changes).length === 0) {
158
+ return current;
159
+ }
160
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
161
+ const run = db.transaction(() => {
162
+ var _a2;
163
+ updatePayout.run(next.minutes, next.amount, next.note, input.id);
164
+ (0, import_audit.writeAuditLog)(db, {
165
+ atUtc: now,
166
+ actorId: input.actorId,
167
+ action: "payout.update",
168
+ entity: "payout",
169
+ entityId: input.id,
170
+ detail: { changes },
171
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
172
+ });
173
+ });
174
+ run();
175
+ const updated = read(input.id);
176
+ if (!updated) {
177
+ throw new Error(`payout ${input.id} disappeared right after the update`);
178
+ }
179
+ return updated;
180
+ },
181
+ remove(input) {
182
+ var _a;
183
+ const current = read(input.id);
184
+ if (!current) {
185
+ return false;
186
+ }
187
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
188
+ const run = db.transaction(() => {
189
+ var _a2;
190
+ deletePayout.run(input.id);
191
+ (0, import_audit.writeAuditLog)(db, {
192
+ atUtc: now,
193
+ actorId: input.actorId,
194
+ action: "payout.delete",
195
+ entity: "payout",
196
+ entityId: input.id,
197
+ detail: {
198
+ userId: current.userId,
199
+ year: current.year,
200
+ month: current.month,
201
+ minutes: current.minutes
202
+ },
203
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
204
+ });
205
+ });
206
+ run();
207
+ return true;
208
+ },
209
+ sumMinutes(userId, year, month) {
210
+ const value = month != null ? month : null;
211
+ const row = selectSum.get(userId, requireYear(year), value, value);
212
+ return row.minutes;
213
+ }
214
+ };
215
+ }
216
+ // Annotate the CommonJS export names for ESM import in node:
217
+ 0 && (module.exports = {
218
+ createPayoutsRepository,
219
+ mapPayoutRow
220
+ });
221
+ //# sourceMappingURL=payouts.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/db/repositories/payouts.ts"],
4
+ "sourcesContent": ["/**\n * Payout repository (`payouts`).\n *\n * A payout records overtime that was paid out instead of being taken as time off. It is attached to a year\n * and optionally to a month: the aggregation service subtracts the payouts of a year from the overtime\n * balance. Positive minutes mean \u201Cpaid out\u201D, negative ones are corrections (e.g. a repayment).\n */\n\nimport type { Db } from \"../database\";\nimport { NotFoundError, ValidationError } from \"../../errors\";\nimport { diffFields, writeAuditLog } from \"./audit\";\n\n/** A payout as stored in the database. */\nexport interface PayoutRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Employee the payout belongs to */\n\tuserId: number;\n\t/** Four digit year the payout belongs to */\n\tyear: number;\n\t/** Month (1 to 12) or `null` for a payout of the whole year */\n\tmonth: number | null;\n\t/** Paid out overtime in minutes */\n\tminutes: number;\n\t/** Paid amount in the currency of the instance, informational only */\n\tamount: number | null;\n\t/** Free-form note (e.g. the closing of a month) */\n\tnote: string | null;\n\t/** Instant of creation, UTC epoch seconds */\n\tcreatedAt: number;\n\t/** Who recorded the payout */\n\tcreatedBy: number | null;\n}\n\n/** Input for creating a payout. */\nexport interface CreatePayoutInput {\n\t/** Employee the payout belongs to */\n\tuserId: number;\n\t/** Four digit year */\n\tyear: number;\n\t/** Month (1 to 12) or `null` for the whole year */\n\tmonth?: number | null;\n\t/** Paid out overtime in minutes (negative values are corrections) */\n\tminutes: number;\n\t/** Paid amount, informational only */\n\tamount?: number | null;\n\t/** Free-form note */\n\tnote?: string | null;\n\t/** Who records the payout */\n\tactorId: number;\n\t/** Client IP address of the actor */\n\tactorIp?: string | null;\n\t/** Instant of creation, defaults to now */\n\tnow?: number;\n}\n\n/** Input for changing a payout. */\nexport interface UpdatePayoutInput {\n\t/** Id of the changed payout */\n\tid: number;\n\t/** Fields to change */\n\tpatch: {\n\t\tminutes?: number;\n\t\tamount?: number | null;\n\t\tnote?: string | null;\n\t};\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/** Payout storage operations. */\nexport interface PayoutsRepository {\n\t/** Payouts of a user, filtered by year and/or month, newest first */\n\tlist(userId: number, options?: { year?: number; month?: number }): PayoutRecord[];\n\t/** Reads a payout by id */\n\tfindById(id: number): PayoutRecord | null;\n\t/** Records a payout */\n\tcreate(input: CreatePayoutInput): PayoutRecord;\n\t/** Changes a payout and audits the changed fields */\n\tupdate(input: UpdatePayoutInput): PayoutRecord;\n\t/** Deletes a payout */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Sum of the paid out minutes of a year (or of one month inside it) */\n\tsumMinutes(userId: number, year: number, month?: number): number;\n}\n\ninterface PayoutRow {\n\tid: number;\n\tuser_id: number;\n\tyear: number;\n\tmonth: number | null;\n\tminutes: number;\n\tamount: number | null;\n\tnote: string | null;\n\tcreated_at: number;\n\tcreated_by: number | null;\n}\n\nconst COLUMNS = \"id, user_id, year, month, minutes, amount, note, created_at, created_by\";\n\n/** Field names of a payout that are compared for the audit trail. */\nconst AUDITED_FIELDS: (keyof PayoutRecord)[] = [\"minutes\", \"amount\", \"note\"];\n\n/**\n * Maps a database row to a payout record.\n *\n * @param row - raw database row\n * @returns payout record\n */\nexport function mapPayoutRow(row: PayoutRow): PayoutRecord {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tyear: row.year,\n\t\tmonth: row.month,\n\t\tminutes: row.minutes,\n\t\tamount: row.amount,\n\t\tnote: row.note,\n\t\tcreatedAt: row.created_at,\n\t\tcreatedBy: row.created_by,\n\t};\n}\n\n/**\n * Validates the year of a payout.\n *\n * @param year - four digit year\n * @returns the validated year\n */\nfunction requireYear(year: number): number {\n\tif (!Number.isInteger(year) || year < 2000 || year > 2100) {\n\t\tthrow new ValidationError(`year must be a four digit year between 2000 and 2100 (got ${year})`);\n\t}\n\treturn year;\n}\n\n/**\n * Validates the month of a payout.\n *\n * @param month - month (1 to 12) or `null`\n * @returns the validated month\n */\nfunction requireMonth(month: number | null): number | null {\n\tif (month === null) {\n\t\treturn null;\n\t}\n\tif (!Number.isInteger(month) || month < 1 || month > 12) {\n\t\tthrow new ValidationError(`month must be between 1 and 12 or null (got ${month})`);\n\t}\n\treturn month;\n}\n\n/**\n * Validates the paid out minutes.\n *\n * @param minutes - minutes to validate\n * @returns the validated minutes\n */\nfunction requireMinutes(minutes: number): number {\n\tif (!Number.isInteger(minutes) || minutes === 0) {\n\t\tthrow new ValidationError(`minutes must be a whole number and not 0 (got ${minutes})`);\n\t}\n\treturn minutes;\n}\n\n/**\n * Validates the paid amount.\n *\n * @param amount - amount to validate\n * @returns the validated amount\n */\nfunction requireAmount(amount: number | null): number | null {\n\tif (amount === null) {\n\t\treturn null;\n\t}\n\tif (!Number.isFinite(amount)) {\n\t\tthrow new ValidationError(`amount must be a number or null (got ${amount})`);\n\t}\n\treturn amount;\n}\n\n/**\n * Creates the payout repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createPayoutsRepository(db: Db): PayoutsRepository {\n\tconst selectById = db.prepare(`SELECT ${COLUMNS} FROM payouts WHERE id = ?`);\n\tconst selectByUser = db.prepare(\n\t\t`SELECT ${COLUMNS} FROM payouts\n\t\t WHERE user_id = ? AND (? IS NULL OR year = ?) AND (? IS NULL OR month = ?)\n\t\t ORDER BY year DESC, COALESCE(month, 0) DESC, id DESC`,\n\t);\n\tconst selectSum = db.prepare(\n\t\t`SELECT COALESCE(SUM(minutes), 0) AS minutes FROM payouts\n\t\t WHERE user_id = ? AND year = ? AND (? IS NULL OR month = ?)`,\n\t);\n\tconst insertPayout = db.prepare(\n\t\t`INSERT INTO payouts (user_id, year, month, minutes, amount, note, created_at, created_by)\n\t\t VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,\n\t);\n\tconst updatePayout = db.prepare(\"UPDATE payouts SET minutes = ?, amount = ?, note = ? WHERE id = ?\");\n\tconst deletePayout = db.prepare(\"DELETE FROM payouts WHERE id = ?\");\n\n\tconst read = (id: number): PayoutRecord | null => {\n\t\tconst row = selectById.get(id) as PayoutRow | undefined;\n\t\treturn row ? mapPayoutRow(row) : null;\n\t};\n\n\treturn {\n\t\tlist(userId: number, options?: { year?: number; month?: number }): PayoutRecord[] {\n\t\t\tconst year = options?.year ?? null;\n\t\t\tconst month = options?.month ?? null;\n\t\t\tconst rows = selectByUser.all(userId, year, year, month, month) as PayoutRow[];\n\t\t\treturn rows.map(mapPayoutRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tcreate(input: CreatePayoutInput): PayoutRecord {\n\t\t\tconst year = requireYear(input.year);\n\t\t\tconst month = requireMonth(input.month ?? null);\n\t\t\tconst minutes = requireMinutes(input.minutes);\n\t\t\tconst amount = requireAmount(input.amount ?? null);\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tlet payoutId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertPayout.run(\n\t\t\t\t\tinput.userId,\n\t\t\t\t\tyear,\n\t\t\t\t\tmonth,\n\t\t\t\t\tminutes,\n\t\t\t\t\tamount,\n\t\t\t\t\tinput.note ?? null,\n\t\t\t\t\tnow,\n\t\t\t\t\tinput.actorId,\n\t\t\t\t);\n\t\t\t\tpayoutId = 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: \"payout.create\",\n\t\t\t\t\tentity: \"payout\",\n\t\t\t\t\tentityId: payoutId,\n\t\t\t\t\tdetail: { userId: input.userId, year, month, minutes, amount },\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(payoutId);\n\t\t\tif (!created) {\n\t\t\t\tthrow new Error(`payout ${payoutId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn created;\n\t\t},\n\n\t\tupdate(input: UpdatePayoutInput): PayoutRecord {\n\t\t\tconst current = read(input.id);\n\t\t\tif (!current) {\n\t\t\t\tthrow new NotFoundError(`payout ${input.id} not found`);\n\t\t\t}\n\n\t\t\tconst next: PayoutRecord = {\n\t\t\t\t...current,\n\t\t\t\tminutes: input.patch.minutes === undefined ? current.minutes : requireMinutes(input.patch.minutes),\n\t\t\t\tamount: input.patch.amount === undefined ? current.amount : requireAmount(input.patch.amount),\n\t\t\t\tnote: input.patch.note === undefined ? current.note : input.patch.note,\n\t\t\t};\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_FIELDS as unknown as (keyof Record<string, unknown>)[],\n\t\t\t);\n\t\t\tif (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\tupdatePayout.run(next.minutes, next.amount, next.note, 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: \"payout.update\",\n\t\t\t\t\tentity: \"payout\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { 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 updated = read(input.id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(`payout ${input.id} disappeared right after the update`);\n\t\t\t}\n\t\t\treturn updated;\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\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tdeletePayout.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: \"payout.delete\",\n\t\t\t\t\tentity: \"payout\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tuserId: current.userId,\n\t\t\t\t\t\tyear: current.year,\n\t\t\t\t\t\tmonth: current.month,\n\t\t\t\t\t\tminutes: current.minutes,\n\t\t\t\t\t},\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\tsumMinutes(userId: number, year: number, month?: number): number {\n\t\t\tconst value = month ?? null;\n\t\t\tconst row = selectSum.get(userId, requireYear(year), value, value) as { minutes: number };\n\t\t\treturn row.minutes;\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b"]
7
+ }
@@ -0,0 +1,264 @@
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 rfid_exports = {};
20
+ __export(rfid_exports, {
21
+ buildTagToken: () => buildTagToken,
22
+ createRfidRepository: () => createRfidRepository,
23
+ newTagUid: () => newTagUid,
24
+ parseTagToken: () => parseTagToken,
25
+ sameSignature: () => sameSignature,
26
+ signTag: () => signTag
27
+ });
28
+ module.exports = __toCommonJS(rfid_exports);
29
+ var import_node_crypto = require("node:crypto");
30
+ var import_errors = require("../../errors");
31
+ var import_audit = require("./audit");
32
+ function newTagUid() {
33
+ return (0, import_node_crypto.randomBytes)(16).toString("base64url");
34
+ }
35
+ function signTag(secret, uid, userId, expiresAt) {
36
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(`${uid}.${userId}.${expiresAt}`).digest("base64url");
37
+ }
38
+ function buildTagToken(uid, userId, expiresAt, signature) {
39
+ return `${uid}.${userId}.${expiresAt}.${signature}`;
40
+ }
41
+ function parseTagToken(token) {
42
+ const parts = token.trim().split(".");
43
+ if (parts.length !== 4) {
44
+ return null;
45
+ }
46
+ const [uid, rawUserId, rawExpires, signature] = parts;
47
+ const userId = Number(rawUserId);
48
+ const expiresAt = Number(rawExpires);
49
+ if (!uid || !signature || !Number.isInteger(userId) || !Number.isInteger(expiresAt)) {
50
+ return null;
51
+ }
52
+ return { uid, userId, expiresAt, signature };
53
+ }
54
+ function sameSignature(expected, received) {
55
+ const left = Buffer.from(expected);
56
+ const right = Buffer.from(received);
57
+ return left.length === right.length && (0, import_node_crypto.timingSafeEqual)(left, right);
58
+ }
59
+ const TAG_COLUMNS = `id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at`;
60
+ function mapTagRow(row) {
61
+ return {
62
+ id: row.id,
63
+ uid: row.uid,
64
+ userId: row.user_id,
65
+ label: row.label,
66
+ isActive: row.is_active !== 0,
67
+ expiresAt: row.expires_at,
68
+ lastUsedAt: row.last_used_at,
69
+ createdAt: row.created_at
70
+ };
71
+ }
72
+ function createRfidRepository(db) {
73
+ const insertTag = db.prepare(
74
+ `INSERT INTO rfid_tags (uid, token_hash, user_id, label, is_active, expires_at, created_at)
75
+ VALUES (?, ?, ?, ?, 1, ?, ?)`
76
+ );
77
+ const selectById = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE id = ?`);
78
+ const selectByUid = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE uid = ?`);
79
+ const selectAll = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags ORDER BY created_at DESC, id DESC`);
80
+ const selectActive = db.prepare(
81
+ `SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE is_active = 1 ORDER BY created_at DESC, id DESC`
82
+ );
83
+ const updateLastUsed = db.prepare("UPDATE rfid_tags SET last_used_at = ? WHERE id = ?");
84
+ const deactivate = db.prepare("UPDATE rfid_tags SET is_active = 0 WHERE id = ?");
85
+ const deleteTag = db.prepare("DELETE FROM rfid_tags WHERE id = ?");
86
+ const changeFields = db.prepare(
87
+ "UPDATE rfid_tags SET user_id = ?, label = ?, expires_at = ?, is_active = ? WHERE id = ?"
88
+ );
89
+ const changeSignature = db.prepare("UPDATE rfid_tags SET token_hash = ? WHERE id = ?");
90
+ const read = (id) => {
91
+ const row = selectById.get(id);
92
+ return row ? mapTagRow(row) : null;
93
+ };
94
+ return {
95
+ create(input) {
96
+ var _a, _b;
97
+ const uid = input.uid.trim();
98
+ if (!uid) {
99
+ throw new import_errors.ValidationError("uid is required");
100
+ }
101
+ const expiresAt = (_a = input.expiresAt) != null ? _a : null;
102
+ if (expiresAt !== null && (!Number.isInteger(expiresAt) || expiresAt <= 0)) {
103
+ throw new import_errors.ValidationError(`expiresAt must be a positive instant (got ${input.expiresAt})`);
104
+ }
105
+ if (!input.signature) {
106
+ throw new import_errors.ValidationError("signature is required");
107
+ }
108
+ const now = (_b = input.now) != null ? _b : Math.floor(Date.now() / 1e3);
109
+ let tagId = 0;
110
+ const run = db.transaction(() => {
111
+ var _a2, _b2, _c;
112
+ const result = insertTag.run(uid, input.signature, input.userId, (_a2 = input.label) != null ? _a2 : null, expiresAt, now);
113
+ tagId = Number(result.lastInsertRowid);
114
+ (0, import_audit.writeAuditLog)(db, {
115
+ atUtc: now,
116
+ actorId: input.actorId,
117
+ action: "rfid.create",
118
+ entity: "rfid_tag",
119
+ entityId: tagId,
120
+ // the signature is a credential and never part of the audit trail
121
+ detail: { uid, userId: input.userId, label: (_b2 = input.label) != null ? _b2 : null, expiresAt },
122
+ ip: (_c = input.actorIp) != null ? _c : null
123
+ });
124
+ });
125
+ run();
126
+ const tag = read(tagId);
127
+ if (!tag) {
128
+ throw new Error(`tag ${tagId} disappeared right after creation`);
129
+ }
130
+ return tag;
131
+ },
132
+ list(options) {
133
+ const rows = ((options == null ? void 0 : options.includeInactive) === true ? selectAll : selectActive).all();
134
+ return rows.map(mapTagRow);
135
+ },
136
+ findById: read,
137
+ verify(input) {
138
+ var _a;
139
+ const row = selectByUid.get(input.uid.trim());
140
+ if (!row) {
141
+ return null;
142
+ }
143
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
144
+ const tag = mapTagRow(row);
145
+ if (!tag.isActive) {
146
+ return null;
147
+ }
148
+ if (tag.expiresAt !== null && tag.expiresAt <= now) {
149
+ return null;
150
+ }
151
+ if (tag.userId !== input.userId) {
152
+ return null;
153
+ }
154
+ return sameSignature(row.token_hash, input.signature) ? tag : null;
155
+ },
156
+ touch(input) {
157
+ var _a;
158
+ updateLastUsed.run((_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3), input.id);
159
+ },
160
+ revoke(input) {
161
+ var _a;
162
+ const tag = read(input.id);
163
+ if (!tag || !tag.isActive) {
164
+ return false;
165
+ }
166
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
167
+ const run = db.transaction(() => {
168
+ var _a2;
169
+ deactivate.run(input.id);
170
+ (0, import_audit.writeAuditLog)(db, {
171
+ atUtc: now,
172
+ actorId: input.actorId,
173
+ action: "rfid.revoke",
174
+ entity: "rfid_tag",
175
+ entityId: input.id,
176
+ detail: { uid: tag.uid, userId: tag.userId },
177
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
178
+ });
179
+ });
180
+ run();
181
+ return true;
182
+ },
183
+ remove(input) {
184
+ var _a;
185
+ const tag = read(input.id);
186
+ if (!tag) {
187
+ return false;
188
+ }
189
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
190
+ const run = db.transaction(() => {
191
+ var _a2;
192
+ deleteTag.run(input.id);
193
+ (0, import_audit.writeAuditLog)(db, {
194
+ atUtc: now,
195
+ actorId: input.actorId,
196
+ action: "rfid.delete",
197
+ entity: "rfid_tag",
198
+ entityId: input.id,
199
+ detail: { uid: tag.uid, userId: tag.userId, isActive: tag.isActive },
200
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
201
+ });
202
+ });
203
+ run();
204
+ return true;
205
+ },
206
+ update(input) {
207
+ var _a;
208
+ const tag = read(input.id);
209
+ if (!tag) {
210
+ return null;
211
+ }
212
+ const userId = input.userId === void 0 ? tag.userId : input.userId;
213
+ if (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {
214
+ throw new import_errors.ValidationError(`userId must be a positive whole number (got ${input.userId})`);
215
+ }
216
+ const expiresAt = input.expiresAt === void 0 ? tag.expiresAt : input.expiresAt;
217
+ if (expiresAt !== null && (!Number.isInteger(expiresAt) || expiresAt <= 0)) {
218
+ throw new import_errors.ValidationError(`expiresAt must be a positive instant (got ${input.expiresAt})`);
219
+ }
220
+ if (input.signature !== void 0 && !input.signature) {
221
+ throw new import_errors.ValidationError("signature must not be empty");
222
+ }
223
+ const label = input.label === void 0 ? tag.label : input.label;
224
+ const isActive = input.activate === void 0 ? tag.isActive : input.activate;
225
+ const now = (_a = input.now) != null ? _a : Math.floor(Date.now() / 1e3);
226
+ const run = db.transaction(() => {
227
+ var _a2;
228
+ changeFields.run(userId, label, expiresAt, isActive ? 1 : 0, input.id);
229
+ if (input.signature !== void 0) {
230
+ changeSignature.run(input.signature, input.id);
231
+ }
232
+ (0, import_audit.writeAuditLog)(db, {
233
+ atUtc: now,
234
+ actorId: input.actorId,
235
+ action: "rfid.update",
236
+ entity: "rfid_tag",
237
+ entityId: input.id,
238
+ // the signature itself stays out of the audit trail, whether it was replaced is what counts
239
+ detail: {
240
+ uid: tag.uid,
241
+ userId,
242
+ label,
243
+ expiresAt,
244
+ isActive,
245
+ reissued: input.signature !== void 0
246
+ },
247
+ ip: (_a2 = input.actorIp) != null ? _a2 : null
248
+ });
249
+ });
250
+ run();
251
+ return read(input.id);
252
+ }
253
+ };
254
+ }
255
+ // Annotate the CommonJS export names for ESM import in node:
256
+ 0 && (module.exports = {
257
+ buildTagToken,
258
+ createRfidRepository,
259
+ newTagUid,
260
+ parseTagToken,
261
+ sameSignature,
262
+ signTag
263
+ });
264
+ //# sourceMappingURL=rfid.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/lib/db/repositories/rfid.ts"],
4
+ "sourcesContent": ["/**\n * RFID/NFC tags (`rfid_tags`).\n *\n * A tag is a **signed deep link**: `uid.userId.exp.signature`, where the signature is\n * `HMAC-SHA256(hmacSecret, \"uid.userId.exp\")`. That way a tag needs no login on the scanning phone, cannot be\n * guessed (a forged payload fails the signature) and can be revoked or given an expiry. The stored `token_hash`\n * is the signature itself; the extra `uid` row keeps the lookup cheap and lets a tag be revoked individually.\n */\n\nimport { createHmac, randomBytes, timingSafeEqual } from \"node:crypto\";\nimport type { Db } from \"../database\";\nimport { ValidationError } from \"../../errors\";\nimport { writeAuditLog } from \"./audit\";\n\n/** A tag as it is stored (without the signature). */\nexport interface RfidTagRecord {\n\t/** Primary key */\n\tid: number;\n\t/** Identifier of the tag (`uid` of the NFC tag or a generated value) */\n\tuid: string;\n\t/** Owner of the tag, `null` after the user was deleted */\n\tuserId: number | null;\n\t/** Free-form label */\n\tlabel: string | null;\n\t/** False for revoked tags */\n\tisActive: boolean;\n\t/** Instant the tag stops working, `null` = never */\n\texpiresAt: number | null;\n\t/** Instant of the last scan, `null` if never used */\n\tlastUsedAt: number | null;\n\t/** Instant of creation, UTC epoch seconds */\n\tcreatedAt: number;\n}\n\n/** Tag storage operations. */\nexport interface RfidRepository {\n\t/** Creates a tag and audits it (the signature is never written to the audit trail) */\n\tcreate(input: {\n\t\tuid: string;\n\t\tuserId: number;\n\t\tsignature: string;\n\t\tlabel?: string | null;\n\t\t/** Instant the tag stops working, `null` = never */\n\t\texpiresAt?: number | null;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): RfidTagRecord;\n\t/** All tags, optionally including the revoked ones */\n\tlist(options?: { includeInactive?: boolean }): RfidTagRecord[];\n\t/** Reads a tag by id */\n\tfindById(id: number): RfidTagRecord | null;\n\t/** Checks a scanned token and returns the tag when signature, activity, owner and expiry match */\n\tverify(input: { uid: string; userId: number; signature: string; now?: number }): RfidTagRecord | null;\n\t/** Records a successful scan */\n\ttouch(input: { id: number; now?: number }): void;\n\t/** Revokes a tag */\n\trevoke(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/** Deletes a tag for good \u2014 for a revoked badge that should disappear from the list */\n\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean;\n\t/**\n\t * Changes label, owner, expiry and \u2014 when a new signature is passed \u2014 the link of a badge.\n\t *\n\t * The signature binds `uid`, owner and expiry, so handing in `signature` is what issues a new link: the stored\n\t * hash is replaced and every link given out before stops working immediately. `activate` brings a revoked badge\n\t * back into use.\n\t */\n\tupdate(input: {\n\t\tid: number;\n\t\tlabel?: string | null;\n\t\tuserId?: number | null;\n\t\texpiresAt?: number | null;\n\t\tsignature?: string;\n\t\tactivate?: boolean;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): RfidTagRecord | null;\n}\n\n/**\n * Creates a random tag identifier.\n *\n * @returns base64url encoded identifier\n */\nexport function newTagUid(): string {\n\treturn randomBytes(16).toString(\"base64url\");\n}\n\n/**\n * Signs a tag payload.\n *\n * @param secret - instance secret (`hmacSecret`)\n * @param uid - tag identifier\n * @param userId - owner of the tag\n * @param expiresAt - expiry of the payload, UTC epoch seconds\n * @returns base64url encoded signature\n */\nexport function signTag(secret: string, uid: string, userId: number, expiresAt: number): string {\n\treturn createHmac(\"sha256\", secret).update(`${uid}.${userId}.${expiresAt}`).digest(\"base64url\");\n}\n\n/**\n * Builds the token a tag carries.\n *\n * @param uid - tag identifier\n * @param userId - owner of the tag\n * @param expiresAt - expiry of the payload\n * @param signature - signature of the payload\n * @returns the token\n */\nexport function buildTagToken(uid: string, userId: number, expiresAt: number, signature: string): string {\n\treturn `${uid}.${userId}.${expiresAt}.${signature}`;\n}\n\n/**\n * Splits a scanned token.\n *\n * @param token - token from the deep link or the QR code\n * @returns the parsed payload or `null` when the token is malformed\n */\nexport function parseTagToken(\n\ttoken: string,\n): { uid: string; userId: number; expiresAt: number; signature: string } | null {\n\tconst parts = token.trim().split(\".\");\n\tif (parts.length !== 4) {\n\t\treturn null;\n\t}\n\tconst [uid, rawUserId, rawExpires, signature] = parts;\n\tconst userId = Number(rawUserId);\n\tconst expiresAt = Number(rawExpires);\n\tif (!uid || !signature || !Number.isInteger(userId) || !Number.isInteger(expiresAt)) {\n\t\treturn null;\n\t}\n\treturn { uid, userId, expiresAt, signature };\n}\n\n/**\n * Compares two signatures without leaking the position of the first difference.\n *\n * @param expected - signature computed by the server\n * @param received - signature from the token\n * @returns true when both are identical\n */\nexport function sameSignature(expected: string, received: string): boolean {\n\tconst left = Buffer.from(expected);\n\tconst right = Buffer.from(received);\n\treturn left.length === right.length && timingSafeEqual(left, right);\n}\n\ninterface RfidRow {\n\tid: number;\n\tuid: string;\n\ttoken_hash: string;\n\tuser_id: number | null;\n\tlabel: string | null;\n\tis_active: number;\n\texpires_at: number | null;\n\tlast_used_at: number | null;\n\tcreated_at: number;\n}\n\nconst TAG_COLUMNS = `id, uid, token_hash, user_id, label, is_active, expires_at, last_used_at, created_at`;\n\n/**\n * Maps a database row to a tag record (the signature stays inside the repository).\n *\n * @param row - raw database row\n * @returns tag record\n */\nfunction mapTagRow(row: RfidRow): RfidTagRecord {\n\treturn {\n\t\tid: row.id,\n\t\tuid: row.uid,\n\t\tuserId: row.user_id,\n\t\tlabel: row.label,\n\t\tisActive: row.is_active !== 0,\n\t\texpiresAt: row.expires_at,\n\t\tlastUsedAt: row.last_used_at,\n\t\tcreatedAt: row.created_at,\n\t};\n}\n\n/**\n * Creates the tag repository.\n *\n * @param db - open database handle\n * @returns repository instance\n */\nexport function createRfidRepository(db: Db): RfidRepository {\n\tconst insertTag = db.prepare(\n\t\t`INSERT INTO rfid_tags (uid, token_hash, user_id, label, is_active, expires_at, created_at)\n\t\t VALUES (?, ?, ?, ?, 1, ?, ?)`,\n\t);\n\tconst selectById = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE id = ?`);\n\tconst selectByUid = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE uid = ?`);\n\tconst selectAll = db.prepare(`SELECT ${TAG_COLUMNS} FROM rfid_tags ORDER BY created_at DESC, id DESC`);\n\tconst selectActive = db.prepare(\n\t\t`SELECT ${TAG_COLUMNS} FROM rfid_tags WHERE is_active = 1 ORDER BY created_at DESC, id DESC`,\n\t);\n\tconst updateLastUsed = db.prepare(\"UPDATE rfid_tags SET last_used_at = ? WHERE id = ?\");\n\tconst deactivate = db.prepare(\"UPDATE rfid_tags SET is_active = 0 WHERE id = ?\");\n\tconst deleteTag = db.prepare(\"DELETE FROM rfid_tags WHERE id = ?\");\n\t// the visible fields and the credential are changed separately: a label edit must not touch the signature\n\tconst changeFields = db.prepare(\n\t\t\"UPDATE rfid_tags SET user_id = ?, label = ?, expires_at = ?, is_active = ? WHERE id = ?\",\n\t);\n\tconst changeSignature = db.prepare(\"UPDATE rfid_tags SET token_hash = ? WHERE id = ?\");\n\n\t/**\n\t * Reads a tag.\n\t *\n\t * @param id - tag id\n\t * @returns tag or `null`\n\t */\n\tconst read = (id: number): RfidTagRecord | null => {\n\t\tconst row = selectById.get(id) as RfidRow | undefined;\n\t\treturn row ? mapTagRow(row) : null;\n\t};\n\n\treturn {\n\t\tcreate(input: {\n\t\t\tuid: string;\n\t\t\tuserId: number;\n\t\t\tsignature: string;\n\t\t\tlabel?: string | null;\n\t\t\texpiresAt?: number | null;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): RfidTagRecord {\n\t\t\tconst uid = input.uid.trim();\n\t\t\tif (!uid) {\n\t\t\t\tthrow new ValidationError(\"uid is required\");\n\t\t\t}\n\t\t\t// the caller passes the expiry it signed, so the stored value and the token cannot drift apart\n\t\t\tconst expiresAt = input.expiresAt ?? null;\n\t\t\tif (expiresAt !== null && (!Number.isInteger(expiresAt) || expiresAt <= 0)) {\n\t\t\t\tthrow new ValidationError(`expiresAt must be a positive instant (got ${input.expiresAt})`);\n\t\t\t}\n\t\t\tif (!input.signature) {\n\t\t\t\tthrow new ValidationError(\"signature is required\");\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tlet tagId = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tconst result = insertTag.run(uid, input.signature, input.userId, input.label ?? null, expiresAt, now);\n\t\t\t\ttagId = 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: \"rfid.create\",\n\t\t\t\t\tentity: \"rfid_tag\",\n\t\t\t\t\tentityId: tagId,\n\t\t\t\t\t// the signature is a credential and never part of the audit trail\n\t\t\t\t\tdetail: { uid, userId: input.userId, label: input.label ?? null, expiresAt },\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 tag = read(tagId);\n\t\t\tif (!tag) {\n\t\t\t\tthrow new Error(`tag ${tagId} disappeared right after creation`);\n\t\t\t}\n\t\t\treturn tag;\n\t\t},\n\n\t\tlist(options?: { includeInactive?: boolean }): RfidTagRecord[] {\n\t\t\tconst rows = (options?.includeInactive === true ? selectAll : selectActive).all() as RfidRow[];\n\t\t\treturn rows.map(mapTagRow);\n\t\t},\n\n\t\tfindById: read,\n\n\t\tverify(input: { uid: string; userId: number; signature: string; now?: number }): RfidTagRecord | null {\n\t\t\tconst row = selectByUid.get(input.uid.trim()) as RfidRow | undefined;\n\t\t\tif (!row) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst tag = mapTagRow(row);\n\t\t\tif (!tag.isActive) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// an expired tag (or payload) is worthless even when the signature is correct\n\t\t\tif (tag.expiresAt !== null && tag.expiresAt <= now) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tif (tag.userId !== input.userId) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\t// the stored signature has to match the signature of the scanned payload\n\t\t\treturn sameSignature(row.token_hash, input.signature) ? tag : null;\n\t\t},\n\n\t\ttouch(input: { id: number; now?: number }): void {\n\t\t\tupdateLastUsed.run(input.now ?? Math.floor(Date.now() / 1000), input.id);\n\t\t},\n\n\t\trevoke(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst tag = read(input.id);\n\t\t\tif (!tag || !tag.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: \"rfid.revoke\",\n\t\t\t\t\tentity: \"rfid_tag\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { uid: tag.uid, userId: tag.userId },\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\tremove(input: { id: number; actorId: number; actorIp?: string | null; now?: number }): boolean {\n\t\t\tconst tag = read(input.id);\n\t\t\tif (!tag) {\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\tdeleteTag.run(input.id);\n\t\t\t\t// the row is gone, the audit trail keeps the trace of it\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: \"rfid.delete\",\n\t\t\t\t\tentity: \"rfid_tag\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\tdetail: { uid: tag.uid, userId: tag.userId, isActive: tag.isActive },\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\tupdate(input: {\n\t\t\tid: number;\n\t\t\tlabel?: string | null;\n\t\t\tuserId?: number | null;\n\t\t\texpiresAt?: number | null;\n\t\t\tsignature?: string;\n\t\t\tactivate?: boolean;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): RfidTagRecord | null {\n\t\t\tconst tag = read(input.id);\n\t\t\tif (!tag) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst userId = input.userId === undefined ? tag.userId : input.userId;\n\t\t\tif (userId !== null && (!Number.isInteger(userId) || userId <= 0)) {\n\t\t\t\tthrow new ValidationError(`userId must be a positive whole number (got ${input.userId})`);\n\t\t\t}\n\t\t\tconst expiresAt = input.expiresAt === undefined ? tag.expiresAt : input.expiresAt;\n\t\t\tif (expiresAt !== null && (!Number.isInteger(expiresAt) || expiresAt <= 0)) {\n\t\t\t\tthrow new ValidationError(`expiresAt must be a positive instant (got ${input.expiresAt})`);\n\t\t\t}\n\t\t\t// a new link always carries a signature: an empty one would leave a signatureless badge behind\n\t\t\tif (input.signature !== undefined && !input.signature) {\n\t\t\t\tthrow new ValidationError(\"signature must not be empty\");\n\t\t\t}\n\t\t\tconst label = input.label === undefined ? tag.label : input.label;\n\t\t\tconst isActive = input.activate === undefined ? tag.isActive : input.activate;\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tchangeFields.run(userId, label, expiresAt, isActive ? 1 : 0, input.id);\n\t\t\t\tif (input.signature !== undefined) {\n\t\t\t\t\tchangeSignature.run(input.signature, input.id);\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: \"rfid.update\",\n\t\t\t\t\tentity: \"rfid_tag\",\n\t\t\t\t\tentityId: input.id,\n\t\t\t\t\t// the signature itself stays out of the audit trail, whether it was replaced is what counts\n\t\t\t\t\tdetail: {\n\t\t\t\t\t\tuid: tag.uid,\n\t\t\t\t\t\tuserId,\n\t\t\t\t\t\tlabel,\n\t\t\t\t\t\texpiresAt,\n\t\t\t\t\t\tisActive,\n\t\t\t\t\t\treissued: input.signature !== undefined,\n\t\t\t\t\t},\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 read(input.id);\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b"]
7
+ }