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,381 @@
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 auth_exports = {};
20
+ __export(auth_exports, {
21
+ checkPasswordPolicy: () => checkPasswordPolicy,
22
+ createAuthService: () => createAuthService,
23
+ hashPassword: () => hashPassword,
24
+ verifyPassword: () => verifyPassword
25
+ });
26
+ module.exports = __toCommonJS(auth_exports);
27
+ var import_node_crypto = require("node:crypto");
28
+ var import_errors = require("../errors");
29
+ var import_audit = require("../db/repositories/audit");
30
+ const DEFAULT_HASH = {
31
+ cost: 16384,
32
+ blockSize: 8,
33
+ parallelization: 1,
34
+ keyLength: 64
35
+ };
36
+ const HASH_PREFIX = "scrypt";
37
+ function hashPassword(password, options = {}) {
38
+ var _a, _b, _c, _d;
39
+ const cost = (_a = options.cost) != null ? _a : DEFAULT_HASH.cost;
40
+ const blockSize = (_b = options.blockSize) != null ? _b : DEFAULT_HASH.blockSize;
41
+ const parallelization = (_c = options.parallelization) != null ? _c : DEFAULT_HASH.parallelization;
42
+ const keyLength = (_d = options.keyLength) != null ? _d : DEFAULT_HASH.keyLength;
43
+ const salt = (0, import_node_crypto.randomBytes)(16);
44
+ const derived = (0, import_node_crypto.scryptSync)(password, salt, keyLength, {
45
+ N: cost,
46
+ r: blockSize,
47
+ p: parallelization,
48
+ maxmem: 256 * cost * blockSize
49
+ });
50
+ return [
51
+ HASH_PREFIX,
52
+ cost,
53
+ blockSize,
54
+ parallelization,
55
+ keyLength,
56
+ salt.toString("base64"),
57
+ derived.toString("base64")
58
+ ].join("$");
59
+ }
60
+ function verifyPassword(password, stored) {
61
+ const parts = (stored != null ? stored : "").split("$");
62
+ if (parts.length !== 7 || parts[0] !== HASH_PREFIX) {
63
+ return false;
64
+ }
65
+ const cost = Number(parts[1]);
66
+ const blockSize = Number(parts[2]);
67
+ const parallelization = Number(parts[3]);
68
+ const keyLength = Number(parts[4]);
69
+ if (![cost, blockSize, parallelization, keyLength].every((value) => Number.isInteger(value) && value > 0)) {
70
+ return false;
71
+ }
72
+ let expected;
73
+ try {
74
+ expected = Buffer.from(parts[6], "base64");
75
+ } catch {
76
+ return false;
77
+ }
78
+ if (expected.length !== keyLength) {
79
+ return false;
80
+ }
81
+ const derived = (0, import_node_crypto.scryptSync)(password, Buffer.from(parts[5], "base64"), keyLength, {
82
+ N: cost,
83
+ r: blockSize,
84
+ p: parallelization,
85
+ maxmem: 256 * cost * blockSize
86
+ });
87
+ return (0, import_node_crypto.timingSafeEqual)(derived, expected);
88
+ }
89
+ function checkPasswordPolicy(password, policy = {}) {
90
+ var _a, _b, _c;
91
+ const minLength = (_a = policy.minLength) != null ? _a : 10;
92
+ if (password.length < minLength) {
93
+ return "too_short";
94
+ }
95
+ if (((_b = policy.requireLetter) != null ? _b : true) && !/[A-Za-z]/.test(password)) {
96
+ return "no_letter";
97
+ }
98
+ if (((_c = policy.requireDigit) != null ? _c : true) && !/\d/.test(password)) {
99
+ return "no_digit";
100
+ }
101
+ return null;
102
+ }
103
+ function createAuthService(deps) {
104
+ var _a, _b, _c, _d;
105
+ const { db, users, settings, secret } = deps;
106
+ const defaultTtl = (_a = deps.defaultTtlMinutes) != null ? _a : 720;
107
+ const maxFailed = (_b = deps.maxFailedAttempts) != null ? _b : 10;
108
+ const lockoutSeconds = ((_c = deps.lockoutMinutes) != null ? _c : 15) * 60;
109
+ const policy = (_d = deps.policy) != null ? _d : {};
110
+ const failures = /* @__PURE__ */ new Map();
111
+ const selectSession = db.prepare(
112
+ `SELECT id, user_id AS userId, created_at AS createdAt, expires_at AS expiresAt,
113
+ user_agent AS userAgent, ip, revoked
114
+ FROM sessions WHERE id = ?`
115
+ );
116
+ const selectUserSessions = db.prepare(
117
+ `SELECT id, user_id AS userId, created_at AS createdAt, expires_at AS expiresAt,
118
+ user_agent AS userAgent, ip, revoked
119
+ FROM sessions WHERE user_id = ? AND revoked = 0 AND expires_at > ?
120
+ ORDER BY created_at DESC`
121
+ );
122
+ const insertSession = db.prepare(
123
+ `INSERT INTO sessions (id, user_id, created_at, expires_at, user_agent, ip, revoked)
124
+ VALUES (?, ?, ?, ?, ?, ?, 0)`
125
+ );
126
+ const revokeSession = db.prepare("UPDATE sessions SET revoked = 1 WHERE id = ?");
127
+ const revokeUserSessions = db.prepare("UPDATE sessions SET revoked = 1 WHERE user_id = ? AND revoked = 0");
128
+ const extendSession = db.prepare("UPDATE sessions SET expires_at = ? WHERE id = ?");
129
+ const purgeSessions = db.prepare("DELETE FROM sessions WHERE expires_at <= ? OR revoked = 1");
130
+ const hashToken = (token) => (0, import_node_crypto.createHash)("sha256").update(token).digest("hex");
131
+ const ttlSeconds = () => Math.max(1, Math.round(settings.getNumber("session_ttl_minutes", defaultTtl))) * 60;
132
+ const publicUser = (user) => ({
133
+ id: user.id,
134
+ login: user.login,
135
+ displayName: user.displayName,
136
+ locale: user.locale,
137
+ timezone: user.timezone,
138
+ mustChangePw: user.mustChangePw
139
+ });
140
+ const readSession = (sessionId) => {
141
+ const row = selectSession.get(sessionId);
142
+ return row ? { ...row, revoked: row.revoked !== 0 } : null;
143
+ };
144
+ return {
145
+ login(input) {
146
+ var _a2, _b2;
147
+ const now = (_a2 = input.now) != null ? _a2 : Math.floor(Date.now() / 1e3);
148
+ const key = input.login.trim().toLowerCase();
149
+ const previous = failures.get(key);
150
+ if (previous && previous.blockedUntil > now) {
151
+ (0, import_audit.writeAuditLog)(db, {
152
+ atUtc: now,
153
+ action: "auth.login_locked",
154
+ entity: "user",
155
+ entityId: null,
156
+ detail: { login: input.login.trim(), blockedUntil: previous.blockedUntil },
157
+ ip: (_b2 = input.ip) != null ? _b2 : null
158
+ });
159
+ return { ok: false, error: "locked_out" };
160
+ }
161
+ const registerFailure = (reason, userId) => {
162
+ var _a3;
163
+ const expiredLock = previous !== void 0 && previous.blockedUntil > 0 && previous.blockedUntil <= now;
164
+ const count = previous === void 0 || expiredLock ? 1 : previous.count + 1;
165
+ const blockedUntil = count >= maxFailed ? now + lockoutSeconds : 0;
166
+ failures.set(key, { count, blockedUntil });
167
+ (0, import_audit.writeAuditLog)(db, {
168
+ atUtc: now,
169
+ action: "auth.login_failed",
170
+ entity: "user",
171
+ entityId: userId,
172
+ detail: { login: input.login.trim(), reason, attempts: count, blockedUntil },
173
+ ip: (_a3 = input.ip) != null ? _a3 : null
174
+ });
175
+ return { ok: false, error: "invalid_credentials" };
176
+ };
177
+ const user = users.findByLogin(input.login);
178
+ if (!user) {
179
+ return registerFailure("unknown_user", null);
180
+ }
181
+ if (!user.isActive) {
182
+ return registerFailure("inactive_user", user.id);
183
+ }
184
+ if (!user.passwordHash) {
185
+ return registerFailure("no_password", user.id);
186
+ }
187
+ if (!verifyPassword(input.password, user.passwordHash)) {
188
+ return registerFailure("wrong_password", user.id);
189
+ }
190
+ failures.delete(key);
191
+ const authenticated = user;
192
+ const token = (0, import_node_crypto.randomBytes)(32).toString("base64url");
193
+ const sessionId = hashToken(token);
194
+ const expiresAt = now + ttlSeconds();
195
+ const run = db.transaction(() => {
196
+ var _a3, _b3, _c2, _d2;
197
+ insertSession.run(sessionId, user.id, now, expiresAt, (_a3 = input.userAgent) != null ? _a3 : null, (_b3 = input.ip) != null ? _b3 : null);
198
+ (0, import_audit.writeAuditLog)(db, {
199
+ atUtc: now,
200
+ actorId: user.id,
201
+ action: "auth.login",
202
+ entity: "session",
203
+ entityId: sessionId,
204
+ detail: { login: user.login, userAgent: (_c2 = input.userAgent) != null ? _c2 : null },
205
+ ip: (_d2 = input.ip) != null ? _d2 : null
206
+ });
207
+ });
208
+ run();
209
+ users.recordLogin(user.id, now);
210
+ return { ok: true, token, csrfToken: this.csrfToken(token), expiresAt, user: publicUser(authenticated) };
211
+ },
212
+ logout(input) {
213
+ var _a2, _b2, _c2;
214
+ const token = (_b2 = (_a2 = input.token) == null ? void 0 : _a2.trim()) != null ? _b2 : "";
215
+ if (!token) {
216
+ return false;
217
+ }
218
+ const sessionId = hashToken(token);
219
+ const session = readSession(sessionId);
220
+ if (!session || session.revoked) {
221
+ return false;
222
+ }
223
+ const now = (_c2 = input.now) != null ? _c2 : Math.floor(Date.now() / 1e3);
224
+ const run = db.transaction(() => {
225
+ var _a3, _b3;
226
+ revokeSession.run(sessionId);
227
+ (0, import_audit.writeAuditLog)(db, {
228
+ atUtc: now,
229
+ actorId: (_a3 = input.actorId) != null ? _a3 : session.userId,
230
+ action: "auth.logout",
231
+ entity: "session",
232
+ entityId: sessionId,
233
+ detail: { userId: session.userId },
234
+ ip: (_b3 = input.ip) != null ? _b3 : null
235
+ });
236
+ });
237
+ run();
238
+ return true;
239
+ },
240
+ authenticate(input) {
241
+ var _a2, _b2, _c2;
242
+ const now = (_a2 = input.now) != null ? _a2 : Math.floor(Date.now() / 1e3);
243
+ const token = (_c2 = (_b2 = input.token) == null ? void 0 : _b2.trim()) != null ? _c2 : "";
244
+ if (!token) {
245
+ return { ok: false, error: "no_session" };
246
+ }
247
+ const session = readSession(hashToken(token));
248
+ if (!session || session.revoked) {
249
+ return { ok: false, error: "no_session" };
250
+ }
251
+ if (session.expiresAt <= now) {
252
+ return { ok: false, error: "session_expired" };
253
+ }
254
+ const user = users.findById(session.userId);
255
+ if (!user || !user.isActive) {
256
+ return { ok: false, error: "user_inactive" };
257
+ }
258
+ const permissions = users.permissions(user.id);
259
+ if (input.permission && !permissions.includes(input.permission)) {
260
+ return { ok: false, error: "permission_denied" };
261
+ }
262
+ const ttl = ttlSeconds();
263
+ if (session.expiresAt - now < ttl / 2) {
264
+ const next = now + ttl;
265
+ extendSession.run(next, session.id);
266
+ session.expiresAt = next;
267
+ }
268
+ return {
269
+ ok: true,
270
+ context: {
271
+ sessionId: session.id,
272
+ expiresAt: session.expiresAt,
273
+ user: publicUser(user),
274
+ permissions
275
+ }
276
+ };
277
+ },
278
+ revokeSessions(input) {
279
+ var _a2;
280
+ const now = (_a2 = input.now) != null ? _a2 : Math.floor(Date.now() / 1e3);
281
+ let revoked = 0;
282
+ const run = db.transaction(() => {
283
+ revoked = revokeUserSessions.run(input.userId).changes;
284
+ if (revoked > 0) {
285
+ (0, import_audit.writeAuditLog)(db, {
286
+ atUtc: now,
287
+ actorId: input.actorId,
288
+ action: "auth.revoke_all",
289
+ entity: "user",
290
+ entityId: input.userId,
291
+ detail: { sessions: revoked }
292
+ });
293
+ }
294
+ });
295
+ run();
296
+ return revoked;
297
+ },
298
+ sessions(userId, now) {
299
+ const reference = now != null ? now : Math.floor(Date.now() / 1e3);
300
+ const rows = selectUserSessions.all(userId, reference);
301
+ return rows.map((row) => ({ ...row, revoked: row.revoked !== 0 }));
302
+ },
303
+ purge(now) {
304
+ return purgeSessions.run(now != null ? now : Math.floor(Date.now() / 1e3)).changes;
305
+ },
306
+ rotateIfDue(input) {
307
+ var _a2, _b2;
308
+ const now = (_a2 = input.now) != null ? _a2 : Math.floor(Date.now() / 1e3);
309
+ const previousId = hashToken((_b2 = input.token) != null ? _b2 : "");
310
+ const session = readSession(previousId);
311
+ if (!session || session.revoked || session.expiresAt <= now) {
312
+ return null;
313
+ }
314
+ const threshold = Math.max(900, Math.floor(ttlSeconds() / 2));
315
+ if (now - session.createdAt < threshold) {
316
+ return null;
317
+ }
318
+ const token = (0, import_node_crypto.randomBytes)(32).toString("base64url");
319
+ const expiresAt = now + ttlSeconds();
320
+ db.transaction(() => {
321
+ revokeSession.run(previousId);
322
+ insertSession.run(hashToken(token), session.userId, now, expiresAt, session.userAgent, session.ip);
323
+ (0, import_audit.writeAuditLog)(db, {
324
+ atUtc: now,
325
+ action: "auth.session_rotated",
326
+ entity: "user",
327
+ entityId: session.userId,
328
+ detail: { session: hashToken(token) },
329
+ ip: session.ip
330
+ });
331
+ })();
332
+ return { token, expiresAt };
333
+ },
334
+ csrfToken(token) {
335
+ return (0, import_node_crypto.createHmac)("sha256", secret).update(token).digest("base64url");
336
+ },
337
+ verifyCsrf(input) {
338
+ var _a2, _b2;
339
+ const expected = this.csrfToken((_a2 = input.token) != null ? _a2 : "");
340
+ const provided = (_b2 = input.csrfToken) != null ? _b2 : "";
341
+ if (expected.length !== provided.length) {
342
+ return false;
343
+ }
344
+ return (0, import_node_crypto.timingSafeEqual)(Buffer.from(expected), Buffer.from(provided));
345
+ },
346
+ setPassword(input) {
347
+ var _a2, _b2, _c2, _d2;
348
+ const violated = checkPasswordPolicy(input.password, policy);
349
+ if (violated) {
350
+ throw new import_errors.ValidationError(`password does not fulfil the policy (${violated})`);
351
+ }
352
+ const now = (_a2 = input.now) != null ? _a2 : Math.floor(Date.now() / 1e3);
353
+ const mustChangePw = (_b2 = input.mustChangePw) != null ? _b2 : false;
354
+ users.update({
355
+ id: input.userId,
356
+ patch: { passwordHash: hashPassword(input.password), mustChangePw },
357
+ reason: mustChangePw ? "password reset by an administrator" : "password changed",
358
+ actorId: input.actorId,
359
+ actorIp: (_c2 = input.actorIp) != null ? _c2 : null,
360
+ now
361
+ });
362
+ (0, import_audit.writeAuditLog)(db, {
363
+ atUtc: now,
364
+ actorId: input.actorId,
365
+ action: "auth.password_set",
366
+ entity: "user",
367
+ entityId: input.userId,
368
+ detail: { mustChangePw },
369
+ ip: (_d2 = input.actorIp) != null ? _d2 : null
370
+ });
371
+ }
372
+ };
373
+ }
374
+ // Annotate the CommonJS export names for ESM import in node:
375
+ 0 && (module.exports = {
376
+ checkPasswordPolicy,
377
+ createAuthService,
378
+ hashPassword,
379
+ verifyPassword
380
+ });
381
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/lib/services/auth.ts"],
4
+ "sourcesContent": ["/**\n * Authentication and session service.\n *\n * Passwords are stored as **scrypt** hashes (params are part of the stored string, so the cost can be raised\n * later without invalidating existing hashes). Sessions use opaque random tokens; only the SHA-256 hash of a\n * token is stored, so a leaked database never exposes usable session tokens.\n *\n * Every login, failed attempt, lockout and logout is audited. Failed attempts are counted per login name and\n * lock the account for a while (process-local, which is what a single adapter instance needs).\n */\n\nimport { createHash, createHmac, randomBytes, scryptSync, timingSafeEqual } from \"node:crypto\";\nimport type { Db } from \"../db/database\";\nimport type { SettingsRepository } from \"../db/repositories/settings\";\nimport type { UserRecord, UsersRepository } from \"../db/repositories/users\";\nimport { ValidationError } from \"../errors\";\nimport { writeAuditLog } from \"../db/repositories/audit\";\n\n/** Cost parameters of a stored password hash. */\nexport interface PasswordHashOptions {\n\t/** CPU/memory cost (`N`) */\n\tcost?: number;\n\t/** Block size (`r`) */\n\tblockSize?: number;\n\t/** Parallelisation (`p`) */\n\tparallelization?: number;\n\t/** Length of the derived key in bytes */\n\tkeyLength?: number;\n}\n\n/** Rules a new password has to fulfil. */\nexport interface PasswordPolicy {\n\t/** Minimum length, default 10 */\n\tminLength?: number;\n\t/** Require at least one digit, default true */\n\trequireDigit?: boolean;\n\t/** Require at least one letter, default true */\n\trequireLetter?: boolean;\n}\n\n/** A user as it may leave the server (never contains hashes). */\nexport interface PublicUser {\n\t/** Primary key */\n\tid: number;\n\t/** Login name */\n\tlogin: string;\n\t/** Shown name */\n\tdisplayName: string;\n\t/** Preferred language */\n\tlocale: string;\n\t/** IANA time zone */\n\ttimezone: string;\n\t/** True while the user has to set a new password */\n\tmustChangePw: boolean;\n}\n\n/** Input for a login. */\nexport interface LoginInput {\n\t/** Login name (case insensitive) */\n\tlogin: string;\n\t/** Plain password */\n\tpassword: string;\n\t/** Browser/device information, stored with the session */\n\tuserAgent?: string | null;\n\t/** Client IP address */\n\tip?: string | null;\n\t/** Instant of the login, defaults to now */\n\tnow?: number;\n}\n\n/** Result of a login attempt. */\nexport type LoginResult =\n\t| {\n\t\t\t/** Login succeeded */\n\t\t\tok: true;\n\t\t\t/** Session token (only returned once, never stored in plain form) */\n\t\t\ttoken: string;\n\t\t\t/** Token that has to be sent back for state changing requests */\n\t\t\tcsrfToken: string;\n\t\t\t/** Instant the session expires, UTC epoch seconds */\n\t\t\texpiresAt: number;\n\t\t\t/** Authenticated user without hashes */\n\t\t\tuser: PublicUser;\n\t }\n\t| {\n\t\t\t/** Login failed */\n\t\t\tok: false;\n\t\t\t/** Reason, always generic towards the client */\n\t\t\terror: \"invalid_credentials\" | \"locked_out\";\n\t };\n\n/** Reason why a request was not authenticated. */\nexport type AuthError = \"no_session\" | \"session_expired\" | \"user_inactive\" | \"permission_denied\";\n\n/** Result of authenticating a request. */\nexport type AuthResult =\n\t| {\n\t\t\t/** Session is valid */\n\t\t\tok: true;\n\t\t\t/** Authenticated context */\n\t\t\tcontext: AuthContext;\n\t }\n\t| {\n\t\t\t/** Session is not usable */\n\t\t\tok: false;\n\t\t\t/** Reason of the rejection */\n\t\t\terror: AuthError;\n\t };\n\n/** Everything a request handler needs to know about the caller. */\nexport interface AuthContext {\n\t/** Session id (hash of the token) */\n\tsessionId: string;\n\t/** Instant the session expires */\n\texpiresAt: number;\n\t/** Authenticated user without hashes */\n\tuser: PublicUser;\n\t/** Effective permission keys of the user */\n\tpermissions: string[];\n}\n\n/** A session as shown to an administrator (without the token). */\nexport interface SessionInfo {\n\t/** Session id (hash of the token) */\n\tid: string;\n\t/** Owner of the session */\n\tuserId: number;\n\t/** Instant of creation */\n\tcreatedAt: number;\n\t/** Instant of expiry */\n\texpiresAt: number;\n\t/** Browser/device information */\n\tuserAgent: string | null;\n\t/** Client IP address */\n\tip: string | null;\n\t/** True when the session was ended */\n\trevoked: boolean;\n}\n\n/** Data sources of the authentication service. */\nexport interface AuthDeps {\n\t/** Open database handle */\n\tdb: Db;\n\t/** User storage */\n\tusers: UsersRepository;\n\t/** Instance settings (session lifetime) */\n\tsettings: SettingsRepository;\n\t/** Secret used to sign CSRF tokens */\n\tsecret: string;\n\t/** Session lifetime in minutes when the setting is missing (default 720) */\n\tdefaultTtlMinutes?: number;\n\t/** Failed attempts before a login is locked (default 10) */\n\tmaxFailedAttempts?: number;\n\t/** Minutes a login stays locked (default 15) */\n\tlockoutMinutes?: number;\n\t/** Policy for new passwords */\n\tpolicy?: PasswordPolicy;\n}\n\n/** Authentication operations. */\nexport interface AuthService {\n\t/** Verifies a login and opens a session */\n\tlogin(input: LoginInput): LoginResult;\n\t/** Ends a session */\n\tlogout(input: { token: string; actorId?: number | null; ip?: string | null; now?: number }): boolean;\n\t/** Checks a session token, optionally requiring a permission */\n\tauthenticate(input: { token: string; permission?: string; now?: number }): AuthResult;\n\t/**\n\t * Replaces the token of a session that has been in use for a while (specification 4.10: rotation).\n\t *\n\t * @returns the new token and its expiry, or `null` while the session is too young\n\t */\n\trotateIfDue(input: { token: string; now?: number }): { token: string; expiresAt: number } | null;\n\t/** Ends all sessions of a user (e.g. after a password change) */\n\trevokeSessions(input: { userId: number; actorId: number; now?: number }): number;\n\t/** Active sessions of a user */\n\tsessions(userId: number, now?: number): SessionInfo[];\n\t/** Deletes sessions that expired before the given instant */\n\tpurge(now?: number): number;\n\t/** Token that protects state changing requests of a session */\n\tcsrfToken(token: string): string;\n\t/** Checks a CSRF token */\n\tverifyCsrf(input: { token: string; csrfToken: string }): boolean;\n\t/** Sets a new password (hashed) after checking the policy */\n\tsetPassword(input: {\n\t\tuserId: number;\n\t\tpassword: string;\n\t\tmustChangePw?: boolean;\n\t\tactorId: number;\n\t\tactorIp?: string | null;\n\t\tnow?: number;\n\t}): void;\n}\n\n/** Default cost parameters (interactive login, ~50 ms on a modern machine). */\nconst DEFAULT_HASH: Required<PasswordHashOptions> = {\n\tcost: 16384,\n\tblockSize: 8,\n\tparallelization: 1,\n\tkeyLength: 64,\n};\n\nconst HASH_PREFIX = \"scrypt\";\n\n/**\n * Hashes a password with scrypt.\n *\n * @param password - plain password\n * @param options - cost parameters\n * @returns stored representation `scrypt$N$r$p$keyLength$salt$hash`\n */\nexport function hashPassword(password: string, options: PasswordHashOptions = {}): string {\n\tconst cost = options.cost ?? DEFAULT_HASH.cost;\n\tconst blockSize = options.blockSize ?? DEFAULT_HASH.blockSize;\n\tconst parallelization = options.parallelization ?? DEFAULT_HASH.parallelization;\n\tconst keyLength = options.keyLength ?? DEFAULT_HASH.keyLength;\n\tconst salt = randomBytes(16);\n\tconst derived = scryptSync(password, salt, keyLength, {\n\t\tN: cost,\n\t\tr: blockSize,\n\t\tp: parallelization,\n\t\tmaxmem: 256 * cost * blockSize,\n\t});\n\n\treturn [\n\t\tHASH_PREFIX,\n\t\tcost,\n\t\tblockSize,\n\t\tparallelization,\n\t\tkeyLength,\n\t\tsalt.toString(\"base64\"),\n\t\tderived.toString(\"base64\"),\n\t].join(\"$\");\n}\n\n/**\n * Verifies a password against a stored hash.\n *\n * Malformed or unsupported hashes (e.g. an empty column for imported users) never match.\n *\n * @param password - plain password\n * @param stored - stored representation\n * @returns true when the password matches\n */\nexport function verifyPassword(password: string, stored: string): boolean {\n\tconst parts = (stored ?? \"\").split(\"$\");\n\tif (parts.length !== 7 || parts[0] !== HASH_PREFIX) {\n\t\treturn false;\n\t}\n\tconst cost = Number(parts[1]);\n\tconst blockSize = Number(parts[2]);\n\tconst parallelization = Number(parts[3]);\n\tconst keyLength = Number(parts[4]);\n\tif (![cost, blockSize, parallelization, keyLength].every(value => Number.isInteger(value) && value > 0)) {\n\t\treturn false;\n\t}\n\n\tlet expected: Buffer;\n\ttry {\n\t\texpected = Buffer.from(parts[6], \"base64\");\n\t} catch {\n\t\treturn false;\n\t}\n\tif (expected.length !== keyLength) {\n\t\treturn false;\n\t}\n\n\tconst derived = scryptSync(password, Buffer.from(parts[5], \"base64\"), keyLength, {\n\t\tN: cost,\n\t\tr: blockSize,\n\t\tp: parallelization,\n\t\tmaxmem: 256 * cost * blockSize,\n\t});\n\treturn timingSafeEqual(derived, expected);\n}\n\n/**\n * Checks a new password against the policy.\n *\n * @param password - plain password\n * @param policy - rules to apply\n * @returns the violated rule or `null`\n */\nexport function checkPasswordPolicy(password: string, policy: PasswordPolicy = {}): string | null {\n\tconst minLength = policy.minLength ?? 10;\n\tif (password.length < minLength) {\n\t\treturn \"too_short\";\n\t}\n\tif ((policy.requireLetter ?? true) && !/[A-Za-z]/.test(password)) {\n\t\treturn \"no_letter\";\n\t}\n\tif ((policy.requireDigit ?? true) && !/\\d/.test(password)) {\n\t\treturn \"no_digit\";\n\t}\n\treturn null;\n}\n\n/**\n * Creates the authentication service.\n *\n * @param deps - data sources and policy\n * @returns service instance\n */\nexport function createAuthService(deps: AuthDeps): AuthService {\n\tconst { db, users, settings, secret } = deps;\n\tconst defaultTtl = deps.defaultTtlMinutes ?? 720;\n\tconst maxFailed = deps.maxFailedAttempts ?? 10;\n\tconst lockoutSeconds = (deps.lockoutMinutes ?? 15) * 60;\n\tconst policy = deps.policy ?? {};\n\n\t/** Failed attempts per login name (process-local, which is enough for one adapter instance). */\n\tconst failures = new Map<string, { count: number; blockedUntil: number }>();\n\n\tconst selectSession = db.prepare(\n\t\t`SELECT id, user_id AS userId, created_at AS createdAt, expires_at AS expiresAt,\n\t\t user_agent AS userAgent, ip, revoked\n\t\t FROM sessions WHERE id = ?`,\n\t);\n\tconst selectUserSessions = db.prepare(\n\t\t`SELECT id, user_id AS userId, created_at AS createdAt, expires_at AS expiresAt,\n\t\t user_agent AS userAgent, ip, revoked\n\t\t FROM sessions WHERE user_id = ? AND revoked = 0 AND expires_at > ?\n\t\t ORDER BY created_at DESC`,\n\t);\n\tconst insertSession = db.prepare(\n\t\t`INSERT INTO sessions (id, user_id, created_at, expires_at, user_agent, ip, revoked)\n\t\t VALUES (?, ?, ?, ?, ?, ?, 0)`,\n\t);\n\tconst revokeSession = db.prepare(\"UPDATE sessions SET revoked = 1 WHERE id = ?\");\n\tconst revokeUserSessions = db.prepare(\"UPDATE sessions SET revoked = 1 WHERE user_id = ? AND revoked = 0\");\n\tconst extendSession = db.prepare(\"UPDATE sessions SET expires_at = ? WHERE id = ?\");\n\tconst purgeSessions = db.prepare(\"DELETE FROM sessions WHERE expires_at <= ? OR revoked = 1\");\n\n\t/**\n\t * Only the hash of a token is stored, so a database leak cannot be replayed.\n\t *\n\t * @param token - session token as it was handed to the client\n\t * @returns SHA-256 hash used as session id\n\t */\n\tconst hashToken = (token: string): string => createHash(\"sha256\").update(token).digest(\"hex\");\n\n\tconst ttlSeconds = (): number =>\n\t\tMath.max(1, Math.round(settings.getNumber(\"session_ttl_minutes\", defaultTtl))) * 60;\n\n\tconst publicUser = (user: UserRecord): PublicUser => ({\n\t\tid: user.id,\n\t\tlogin: user.login,\n\t\tdisplayName: user.displayName,\n\t\tlocale: user.locale,\n\t\ttimezone: user.timezone,\n\t\tmustChangePw: user.mustChangePw,\n\t});\n\n\tconst readSession = (sessionId: string): SessionInfo | null => {\n\t\tconst row = selectSession.get(sessionId) as (Omit<SessionInfo, \"revoked\"> & { revoked: number }) | undefined;\n\t\treturn row ? { ...row, revoked: row.revoked !== 0 } : null;\n\t};\n\n\treturn {\n\t\tlogin(input: LoginInput): LoginResult {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst key = input.login.trim().toLowerCase();\n\t\t\tconst previous = failures.get(key);\n\n\t\t\tif (previous && previous.blockedUntil > now) {\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\taction: \"auth.login_locked\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: null,\n\t\t\t\t\tdetail: { login: input.login.trim(), blockedUntil: previous.blockedUntil },\n\t\t\t\t\tip: input.ip ?? null,\n\t\t\t\t});\n\t\t\t\treturn { ok: false, error: \"locked_out\" };\n\t\t\t}\n\n\t\t\t/**\n\t\t\t * Counts a failed attempt and locks the login once the limit is reached.\n\t\t\t *\n\t\t\t * @param reason - internal reason (never sent to the client)\n\t\t\t * @param userId - known user id, `null` when the login is unknown\n\t\t\t * @returns the generic failure result\n\t\t\t */\n\t\t\tconst registerFailure = (reason: string, userId: number | null): LoginResult => {\n\t\t\t\tconst expiredLock = previous !== undefined && previous.blockedUntil > 0 && previous.blockedUntil <= now;\n\t\t\t\tconst count = previous === undefined || expiredLock ? 1 : previous.count + 1;\n\t\t\t\tconst blockedUntil = count >= maxFailed ? now + lockoutSeconds : 0;\n\t\t\t\tfailures.set(key, { count, blockedUntil });\n\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\taction: \"auth.login_failed\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: userId,\n\t\t\t\t\tdetail: { login: input.login.trim(), reason, attempts: count, blockedUntil },\n\t\t\t\t\tip: input.ip ?? null,\n\t\t\t\t});\n\t\t\t\treturn { ok: false, error: \"invalid_credentials\" };\n\t\t\t};\n\n\t\t\tconst user = users.findByLogin(input.login);\n\t\t\tif (!user) {\n\t\t\t\treturn registerFailure(\"unknown_user\", null);\n\t\t\t}\n\t\t\tif (!user.isActive) {\n\t\t\t\treturn registerFailure(\"inactive_user\", user.id);\n\t\t\t}\n\t\t\tif (!user.passwordHash) {\n\t\t\t\treturn registerFailure(\"no_password\", user.id);\n\t\t\t}\n\t\t\tif (!verifyPassword(input.password, user.passwordHash)) {\n\t\t\t\treturn registerFailure(\"wrong_password\", user.id);\n\t\t\t}\n\n\t\t\tfailures.delete(key);\n\n\t\t\tconst authenticated = user;\n\n\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\tconst sessionId = hashToken(token);\n\t\t\tconst expiresAt = now + ttlSeconds();\n\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\tinsertSession.run(sessionId, user.id, now, expiresAt, input.userAgent ?? null, input.ip ?? null);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: user.id,\n\t\t\t\t\taction: \"auth.login\",\n\t\t\t\t\tentity: \"session\",\n\t\t\t\t\tentityId: sessionId,\n\t\t\t\t\tdetail: { login: user.login, userAgent: input.userAgent ?? null },\n\t\t\t\t\tip: input.ip ?? null,\n\t\t\t\t});\n\t\t\t});\n\t\t\trun();\n\t\t\tusers.recordLogin(user.id, now);\n\n\t\t\treturn { ok: true, token, csrfToken: this.csrfToken(token), expiresAt, user: publicUser(authenticated) };\n\t\t},\n\n\t\tlogout(input: { token: string; actorId?: number | null; ip?: string | null; now?: number }): boolean {\n\t\t\tconst token = input.token?.trim() ?? \"\";\n\t\t\tif (!token) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\tconst sessionId = hashToken(token);\n\t\t\tconst session = readSession(sessionId);\n\t\t\tif (!session || session.revoked) {\n\t\t\t\treturn false;\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\trevokeSession.run(sessionId);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\tactorId: input.actorId ?? session.userId,\n\t\t\t\t\taction: \"auth.logout\",\n\t\t\t\t\tentity: \"session\",\n\t\t\t\t\tentityId: sessionId,\n\t\t\t\t\tdetail: { userId: session.userId },\n\t\t\t\t\tip: input.ip ?? 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\tauthenticate(input: { token: string; permission?: string; now?: number }): AuthResult {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst token = input.token?.trim() ?? \"\";\n\t\t\tif (!token) {\n\t\t\t\treturn { ok: false, error: \"no_session\" };\n\t\t\t}\n\n\t\t\tconst session = readSession(hashToken(token));\n\t\t\tif (!session || session.revoked) {\n\t\t\t\treturn { ok: false, error: \"no_session\" };\n\t\t\t}\n\t\t\tif (session.expiresAt <= now) {\n\t\t\t\treturn { ok: false, error: \"session_expired\" };\n\t\t\t}\n\n\t\t\tconst user = users.findById(session.userId);\n\t\t\tif (!user || !user.isActive) {\n\t\t\t\treturn { ok: false, error: \"user_inactive\" };\n\t\t\t}\n\n\t\t\tconst permissions = users.permissions(user.id);\n\t\t\tif (input.permission && !permissions.includes(input.permission)) {\n\t\t\t\treturn { ok: false, error: \"permission_denied\" };\n\t\t\t}\n\n\t\t\t// sliding renewal: extend the session in its second half\n\t\t\tconst ttl = ttlSeconds();\n\t\t\tif (session.expiresAt - now < ttl / 2) {\n\t\t\t\tconst next = now + ttl;\n\t\t\t\textendSession.run(next, session.id);\n\t\t\t\tsession.expiresAt = next;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tok: true,\n\t\t\t\tcontext: {\n\t\t\t\t\tsessionId: session.id,\n\t\t\t\t\texpiresAt: session.expiresAt,\n\t\t\t\t\tuser: publicUser(user),\n\t\t\t\t\tpermissions,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\n\t\trevokeSessions(input: { userId: number; actorId: number; now?: number }): number {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tlet revoked = 0;\n\t\t\tconst run = db.transaction((): void => {\n\t\t\t\trevoked = revokeUserSessions.run(input.userId).changes;\n\t\t\t\tif (revoked > 0) {\n\t\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\t\tatUtc: now,\n\t\t\t\t\t\tactorId: input.actorId,\n\t\t\t\t\t\taction: \"auth.revoke_all\",\n\t\t\t\t\t\tentity: \"user\",\n\t\t\t\t\t\tentityId: input.userId,\n\t\t\t\t\t\tdetail: { sessions: revoked },\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t});\n\t\t\trun();\n\t\t\treturn revoked;\n\t\t},\n\n\t\tsessions(userId: number, now?: number): SessionInfo[] {\n\t\t\tconst reference = now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst rows = selectUserSessions.all(userId, reference) as (Omit<SessionInfo, \"revoked\"> & {\n\t\t\t\trevoked: number;\n\t\t\t})[];\n\t\t\treturn rows.map(row => ({ ...row, revoked: row.revoked !== 0 }));\n\t\t},\n\n\t\tpurge(now?: number): number {\n\t\t\treturn purgeSessions.run(now ?? Math.floor(Date.now() / 1000)).changes;\n\t\t},\n\n\t\trotateIfDue(input: { token: string; now?: number }): { token: string; expiresAt: number } | null {\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst previousId = hashToken(input.token ?? \"\");\n\t\t\tconst session = readSession(previousId);\n\t\t\tif (!session || session.revoked || session.expiresAt <= now) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Only sessions that have really been in use for a while are renewed: half of their lifetime, but at\n\t\t\t// least a quarter of an hour. Otherwise every single request of the web app would churn the row.\n\t\t\tconst threshold = Math.max(900, Math.floor(ttlSeconds() / 2));\n\t\t\tif (now - session.createdAt < threshold) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst token = randomBytes(32).toString(\"base64url\");\n\t\t\tconst expiresAt = now + ttlSeconds();\n\t\t\tdb.transaction((): void => {\n\t\t\t\t// the old token dies with its row; the user, the session start and the device stay the same\n\t\t\t\trevokeSession.run(previousId);\n\t\t\t\tinsertSession.run(hashToken(token), session.userId, now, expiresAt, session.userAgent, session.ip);\n\t\t\t\twriteAuditLog(db, {\n\t\t\t\t\tatUtc: now,\n\t\t\t\t\taction: \"auth.session_rotated\",\n\t\t\t\t\tentity: \"user\",\n\t\t\t\t\tentityId: session.userId,\n\t\t\t\t\tdetail: { session: hashToken(token) },\n\t\t\t\t\tip: session.ip,\n\t\t\t\t});\n\t\t\t})();\n\t\t\treturn { token, expiresAt };\n\t\t},\n\n\t\tcsrfToken(token: string): string {\n\t\t\treturn createHmac(\"sha256\", secret).update(token).digest(\"base64url\");\n\t\t},\n\n\t\tverifyCsrf(input: { token: string; csrfToken: string }): boolean {\n\t\t\tconst expected = this.csrfToken(input.token ?? \"\");\n\t\t\tconst provided = input.csrfToken ?? \"\";\n\t\t\tif (expected.length !== provided.length) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t\treturn timingSafeEqual(Buffer.from(expected), Buffer.from(provided));\n\t\t},\n\n\t\tsetPassword(input: {\n\t\t\tuserId: number;\n\t\t\tpassword: string;\n\t\t\tmustChangePw?: boolean;\n\t\t\tactorId: number;\n\t\t\tactorIp?: string | null;\n\t\t\tnow?: number;\n\t\t}): void {\n\t\t\tconst violated = checkPasswordPolicy(input.password, policy);\n\t\t\tif (violated) {\n\t\t\t\tthrow new ValidationError(`password does not fulfil the policy (${violated})`);\n\t\t\t}\n\n\t\t\tconst now = input.now ?? Math.floor(Date.now() / 1000);\n\t\t\tconst mustChangePw = input.mustChangePw ?? false;\n\t\t\t// the user repository audits the change without the hash\n\t\t\tusers.update({\n\t\t\t\tid: input.userId,\n\t\t\t\tpatch: { passwordHash: hashPassword(input.password), mustChangePw },\n\t\t\t\treason: mustChangePw ? \"password reset by an administrator\" : \"password changed\",\n\t\t\t\tactorId: input.actorId,\n\t\t\t\tactorIp: input.actorIp ?? null,\n\t\t\t\tnow,\n\t\t\t});\n\t\t\twriteAuditLog(db, {\n\t\t\t\tatUtc: now,\n\t\t\t\tactorId: input.actorId,\n\t\t\t\taction: \"auth.password_set\",\n\t\t\t\tentity: \"user\",\n\t\t\t\tentityId: input.userId,\n\t\t\t\tdetail: { mustChangePw },\n\t\t\t\tip: input.actorIp ?? null,\n\t\t\t});\n\t\t},\n\t};\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["_a", "_b", "_c", "_d"]
7
+ }