@terreno/api 0.7.2 → 0.8.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 (50) hide show
  1. package/dist/__tests__/{versionCheck.test.js → versionCheckPlugin.test.js} +2 -2
  2. package/dist/api.d.ts +4 -2
  3. package/dist/api.js +7 -2
  4. package/dist/consentApp.d.ts +33 -0
  5. package/dist/consentApp.js +484 -0
  6. package/dist/consentApp.test.d.ts +1 -0
  7. package/dist/consentApp.test.js +1132 -0
  8. package/dist/index.d.ts +6 -0
  9. package/dist/index.js +6 -0
  10. package/dist/models/consentForm.d.ts +2 -0
  11. package/dist/models/consentForm.js +115 -0
  12. package/dist/models/consentResponse.d.ts +2 -0
  13. package/dist/models/consentResponse.js +73 -0
  14. package/dist/models/versionConfig.d.ts +1 -1
  15. package/dist/openApiValidator.js +2 -0
  16. package/dist/populate.d.ts +1 -0
  17. package/dist/populate.js +53 -13
  18. package/dist/syncConsents.d.ts +67 -0
  19. package/dist/syncConsents.js +334 -0
  20. package/dist/syncConsents.test.d.ts +1 -0
  21. package/dist/syncConsents.test.js +249 -0
  22. package/dist/terrenoApp.js +6 -5
  23. package/dist/terrenoPlugin.d.ts +1 -1
  24. package/dist/types/consentForm.d.ts +32 -0
  25. package/dist/types/consentForm.js +2 -0
  26. package/dist/types/consentResponse.d.ts +23 -0
  27. package/dist/types/consentResponse.js +2 -0
  28. package/dist/vendor/wesleytodd-openapi/lib/generate-doc.js +1 -1
  29. package/dist/versionCheckPlugin.d.ts +2 -0
  30. package/dist/versionCheckPlugin.js +3 -6
  31. package/package.json +1 -1
  32. package/src/__tests__/{versionCheck.test.ts → versionCheckPlugin.test.ts} +2 -2
  33. package/src/api.ts +11 -4
  34. package/src/consentApp.test.ts +749 -0
  35. package/src/consentApp.ts +463 -0
  36. package/src/index.ts +6 -0
  37. package/src/models/consentForm.ts +123 -0
  38. package/src/models/consentResponse.ts +78 -0
  39. package/src/models/versionConfig.ts +1 -1
  40. package/src/openApiValidator.ts +2 -0
  41. package/src/populate.ts +33 -0
  42. package/src/syncConsents.test.ts +124 -0
  43. package/src/syncConsents.ts +263 -0
  44. package/src/terrenoApp.ts +6 -6
  45. package/src/terrenoPlugin.ts +1 -1
  46. package/src/types/consentForm.ts +41 -0
  47. package/src/types/consentResponse.ts +34 -0
  48. package/src/vendor/wesleytodd-openapi/lib/generate-doc.js +1 -1
  49. package/src/versionCheckPlugin.ts +5 -6
  50. /package/dist/__tests__/{versionCheck.test.d.ts → versionCheckPlugin.test.d.ts} +0 -0
@@ -131,7 +131,7 @@ var versionCheckPlugin_1 = require("../versionCheckPlugin");
131
131
  case 2:
132
132
  res = _a.sent();
133
133
  (0, bun_test_1.expect)(res.status).toBe(200);
134
- (0, bun_test_1.expect)(res.body).toEqual({ status: "ok" });
134
+ (0, bun_test_1.expect)(res.body).toEqual({ requiredVersion: 50, status: "ok", warningVersion: 100 });
135
135
  return [2 /*return*/];
136
136
  }
137
137
  });
@@ -236,7 +236,7 @@ var versionCheckPlugin_1 = require("../versionCheckPlugin");
236
236
  case 2:
237
237
  res = _a.sent();
238
238
  (0, bun_test_1.expect)(res.status).toBe(200);
239
- (0, bun_test_1.expect)(res.body).toEqual({ status: "ok" });
239
+ (0, bun_test_1.expect)(res.body).toEqual({ requiredVersion: 50, status: "ok", warningVersion: 100 });
240
240
  return [2 /*return*/];
241
241
  }
242
242
  });
package/dist/api.d.ts CHANGED
@@ -108,8 +108,8 @@ export interface ModelRouterOptions<T> {
108
108
  * query, max limit,
109
109
  * or 500. */
110
110
  maxLimit?: number;
111
- /** */
112
- endpoints?: (router: any) => void;
111
+ /** Custom route setup function. Receives the router and optionally the full options (including openApi). */
112
+ endpoints?: (router: any, options?: Partial<ModelRouterOptions<T>>) => void;
113
113
  /**
114
114
  * Hook that runs after `transformer.transform` but before the object is created.
115
115
  * Can update the body fields based on the request or the user.
@@ -234,6 +234,8 @@ export interface ModelRouterRegistration {
234
234
  path: string;
235
235
  /** The Express router containing CRUD endpoints */
236
236
  router: express.Router;
237
+ /** @internal Rebuilds the router with the openApi instance injected into options */
238
+ _buildWithOpenApi: (openApi: any) => express.Router;
237
239
  }
238
240
  /**
239
241
  * Create a set of CRUD routes given a Mongoose model and configuration options.
package/dist/api.js CHANGED
@@ -299,7 +299,12 @@ function modelRouter(pathOrModel, modelOrOptions, maybeOptions) {
299
299
  }
300
300
  var router = _buildModelRouter(model, options);
301
301
  if (path !== undefined) {
302
- return { __type: "modelRouter", path: path, router: router };
302
+ return {
303
+ __type: "modelRouter",
304
+ _buildWithOpenApi: function (openApi) { return _buildModelRouter(model, __assign(__assign({}, options), { openApi: openApi })); },
305
+ path: path,
306
+ router: router,
307
+ };
303
308
  }
304
309
  return router;
305
310
  }
@@ -309,7 +314,7 @@ function _buildModelRouter(model, options) {
309
314
  var router = express_1.default.Router();
310
315
  // Do before the other router options so endpoints take priority.
311
316
  if (options.endpoints) {
312
- options.endpoints(router);
317
+ options.endpoints(router, options);
313
318
  }
314
319
  var responseHandler = (_a = options.responseHandler) !== null && _a !== void 0 ? _a : transformers_1.defaultResponseHandler;
315
320
  // Always install validation middleware — they are no-ops until configureOpenApiValidator() is called
@@ -0,0 +1,33 @@
1
+ /**
2
+ * ConsentApp plugin for @terreno/api.
3
+ *
4
+ * Registers consent form management and user consent response routes as a TerrenoPlugin.
5
+ * Provides admin CRUD for consent forms, read-only access to responses, and user-facing
6
+ * endpoints for fetching pending consents and submitting responses.
7
+ */
8
+ import type express from "express";
9
+ import type { User } from "./auth";
10
+ import type { TerrenoPlugin } from "./terrenoPlugin";
11
+ import type { ConsentFormDocument } from "./types/consentForm";
12
+ export interface ConsentAppOptions {
13
+ auditTrail?: boolean;
14
+ aiConfig?: {
15
+ generateContent: (params: {
16
+ type: string;
17
+ description: string;
18
+ locale: string;
19
+ }) => Promise<string>;
20
+ translateContent: (params: {
21
+ content: string;
22
+ fromLocale: string;
23
+ toLocale: string;
24
+ }) => Promise<string>;
25
+ };
26
+ resolveConsentForms?: (user: User, forms: ConsentFormDocument[]) => ConsentFormDocument[] | Promise<ConsentFormDocument[]>;
27
+ supportedLocales?: string[];
28
+ }
29
+ export declare class ConsentApp implements TerrenoPlugin {
30
+ private options;
31
+ constructor(options?: ConsentAppOptions);
32
+ register(app: express.Application): void;
33
+ }
@@ -0,0 +1,484 @@
1
+ "use strict";
2
+ /**
3
+ * ConsentApp plugin for @terreno/api.
4
+ *
5
+ * Registers consent form management and user consent response routes as a TerrenoPlugin.
6
+ * Provides admin CRUD for consent forms, read-only access to responses, and user-facing
7
+ * endpoints for fetching pending consents and submitting responses.
8
+ */
9
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
10
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
11
+ return new (P || (P = Promise))(function (resolve, reject) {
12
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
13
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
14
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
15
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
16
+ });
17
+ };
18
+ var __generator = (this && this.__generator) || function (thisArg, body) {
19
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
20
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
21
+ function verb(n) { return function (v) { return step([n, v]); }; }
22
+ function step(op) {
23
+ if (f) throw new TypeError("Generator is already executing.");
24
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
25
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
26
+ if (y = 0, t) op = [op[0] & 2, t.value];
27
+ switch (op[0]) {
28
+ case 0: case 1: t = op; break;
29
+ case 4: _.label++; return { value: op[1], done: false };
30
+ case 5: _.label++; y = op[1]; op = [0]; continue;
31
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
32
+ default:
33
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
34
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
35
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
36
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
37
+ if (t[2]) _.ops.pop();
38
+ _.trys.pop(); continue;
39
+ }
40
+ op = body.call(thisArg, _);
41
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
42
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
43
+ }
44
+ };
45
+ var __values = (this && this.__values) || function(o) {
46
+ var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
47
+ if (m) return m.call(o);
48
+ if (o && typeof o.length === "number") return {
49
+ next: function () {
50
+ if (o && i >= o.length) o = void 0;
51
+ return { value: o && o[i++], done: !o };
52
+ }
53
+ };
54
+ throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
55
+ };
56
+ Object.defineProperty(exports, "__esModule", { value: true });
57
+ exports.ConsentApp = void 0;
58
+ var luxon_1 = require("luxon");
59
+ var api_1 = require("./api");
60
+ var auth_1 = require("./auth");
61
+ var errors_1 = require("./errors");
62
+ var logger_1 = require("./logger");
63
+ var consentForm_1 = require("./models/consentForm");
64
+ var consentResponse_1 = require("./models/consentResponse");
65
+ var permissions_1 = require("./permissions");
66
+ var ConsentApp = /** @class */ (function () {
67
+ function ConsentApp(options) {
68
+ if (options === void 0) { options = {}; }
69
+ this.options = options;
70
+ }
71
+ ConsentApp.prototype.register = function (app) {
72
+ var _this = this;
73
+ var _a = this.options, auditTrail = _a.auditTrail, resolveConsentForms = _a.resolveConsentForms, aiConfig = _a.aiConfig;
74
+ // Admin CRUD for consent forms
75
+ app.use("/consent-forms", (0, api_1.modelRouter)(consentForm_1.ConsentForm, {
76
+ endpoints: function (router) {
77
+ if (aiConfig) {
78
+ // POST /consent-forms/generate - generate consent form content with AI
79
+ router.post("/generate", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
80
+ var user, _a, type, description, _b, locale, content;
81
+ return __generator(this, function (_c) {
82
+ switch (_c.label) {
83
+ case 0:
84
+ user = req.user;
85
+ if (!(user === null || user === void 0 ? void 0 : user.admin)) {
86
+ throw new errors_1.APIError({ status: 403, title: "Admin access required" });
87
+ }
88
+ _a = req.body, type = _a.type, description = _a.description, _b = _a.locale, locale = _b === void 0 ? "en" : _b;
89
+ if (!type) {
90
+ throw new errors_1.APIError({ status: 400, title: "type is required" });
91
+ }
92
+ if (!description) {
93
+ throw new errors_1.APIError({ status: 400, title: "description is required" });
94
+ }
95
+ return [4 /*yield*/, aiConfig.generateContent({ description: description, locale: locale, type: type })];
96
+ case 1:
97
+ content = _c.sent();
98
+ logger_1.logger.info("ConsentForm content generated", { locale: locale, type: type });
99
+ return [2 /*return*/, res.json({ data: { content: content } })];
100
+ }
101
+ });
102
+ }); }));
103
+ // POST /consent-forms/translate - translate consent form content with AI
104
+ router.post("/translate", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
105
+ var user, _a, content, fromLocale, toLocale, translated;
106
+ return __generator(this, function (_b) {
107
+ switch (_b.label) {
108
+ case 0:
109
+ user = req.user;
110
+ if (!(user === null || user === void 0 ? void 0 : user.admin)) {
111
+ throw new errors_1.APIError({ status: 403, title: "Admin access required" });
112
+ }
113
+ _a = req.body, content = _a.content, fromLocale = _a.fromLocale, toLocale = _a.toLocale;
114
+ if (!content) {
115
+ throw new errors_1.APIError({ status: 400, title: "content is required" });
116
+ }
117
+ if (!fromLocale) {
118
+ throw new errors_1.APIError({ status: 400, title: "fromLocale is required" });
119
+ }
120
+ if (!toLocale) {
121
+ throw new errors_1.APIError({ status: 400, title: "toLocale is required" });
122
+ }
123
+ return [4 /*yield*/, aiConfig.translateContent({ content: content, fromLocale: fromLocale, toLocale: toLocale })];
124
+ case 1:
125
+ translated = _b.sent();
126
+ logger_1.logger.info("ConsentForm content translated", { fromLocale: fromLocale, toLocale: toLocale });
127
+ return [2 /*return*/, res.json({ data: { content: translated } })];
128
+ }
129
+ });
130
+ }); }));
131
+ }
132
+ // POST /consent-forms/:id/publish - clone form with incremented version and activate
133
+ router.post("/:id/publish", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
134
+ var user, form, newFormData, newForm;
135
+ return __generator(this, function (_a) {
136
+ switch (_a.label) {
137
+ case 0:
138
+ user = req.user;
139
+ if (!(user === null || user === void 0 ? void 0 : user.admin)) {
140
+ throw new errors_1.APIError({ status: 403, title: "Admin access required" });
141
+ }
142
+ return [4 /*yield*/, consentForm_1.ConsentForm.findExactlyOne({ _id: req.params.id })];
143
+ case 1:
144
+ form = _a.sent();
145
+ newFormData = {
146
+ active: true,
147
+ agreeButtonText: form.agreeButtonText,
148
+ allowDecline: form.allowDecline,
149
+ captureSignature: form.captureSignature,
150
+ checkboxes: form.checkboxes,
151
+ content: form.content,
152
+ declineButtonText: form.declineButtonText,
153
+ defaultLocale: form.defaultLocale,
154
+ order: form.order,
155
+ required: form.required,
156
+ requireScrollToBottom: form.requireScrollToBottom,
157
+ slug: form.slug,
158
+ title: form.title,
159
+ type: form.type,
160
+ version: form.version + 1,
161
+ };
162
+ return [4 /*yield*/, consentForm_1.ConsentForm.create(newFormData)];
163
+ case 2:
164
+ newForm = _a.sent();
165
+ // Deactivate all other versions of the same slug
166
+ return [4 /*yield*/, consentForm_1.ConsentForm.updateMany({ _id: { $ne: newForm._id }, slug: form.slug }, { active: false })];
167
+ case 3:
168
+ // Deactivate all other versions of the same slug
169
+ _a.sent();
170
+ logger_1.logger.info("ConsentForm published", {
171
+ newVersion: newForm.version,
172
+ slug: newForm.slug,
173
+ });
174
+ return [2 /*return*/, res.json({ data: newForm })];
175
+ }
176
+ });
177
+ }); }));
178
+ },
179
+ permissions: {
180
+ create: [permissions_1.Permissions.IsAdmin],
181
+ delete: [permissions_1.Permissions.IsAdmin],
182
+ list: [permissions_1.Permissions.IsAdmin],
183
+ read: [permissions_1.Permissions.IsAdmin],
184
+ update: [permissions_1.Permissions.IsAdmin],
185
+ },
186
+ queryFields: ["slug", "type", "active", "version"],
187
+ sort: "order",
188
+ }));
189
+ // Admin read-only access to consent responses
190
+ app.use("/consent-responses", (0, api_1.modelRouter)(consentResponse_1.ConsentResponse, {
191
+ permissions: {
192
+ create: [],
193
+ delete: [],
194
+ list: [permissions_1.Permissions.IsAdmin],
195
+ read: [permissions_1.Permissions.IsAdmin],
196
+ update: [],
197
+ },
198
+ populatePaths: [
199
+ {
200
+ fields: ["title", "slug", "version", "type"],
201
+ path: "consentFormId",
202
+ },
203
+ ],
204
+ }));
205
+ // User-facing consent endpoints
206
+ var router = require("express").Router();
207
+ // GET /consents/pending - fetch pending consent forms for the current user
208
+ router.get("/pending", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
209
+ var user, activeForms, resolvedForms, existingResponses, respondedFormVersions, existingResponses_1, existingResponses_1_1, response, formId, respondedFormIds, respondedForms, formVersionByFormId, respondedForms_1, respondedForms_1_1, form, pendingForms, filteredOutByResolverCount, filteredOutByResponsesCount;
210
+ var e_1, _a, e_2, _b;
211
+ var _c;
212
+ return __generator(this, function (_d) {
213
+ switch (_d.label) {
214
+ case 0:
215
+ user = req.user;
216
+ if (!user) {
217
+ throw new errors_1.APIError({ status: 401, title: "Authentication required" });
218
+ }
219
+ logger_1.logger.debug("Fetching pending consent forms", { userId: user.id });
220
+ return [4 /*yield*/, consentForm_1.ConsentForm.find({ active: true }).sort({ order: 1 })];
221
+ case 1:
222
+ activeForms = _d.sent();
223
+ if (!resolveConsentForms) return [3 /*break*/, 3];
224
+ return [4 /*yield*/, resolveConsentForms(user, activeForms)];
225
+ case 2:
226
+ resolvedForms = _d.sent();
227
+ logger_1.logger.debug("resolveConsentForms applied", {
228
+ activeFormCount: activeForms.length,
229
+ resolvedFormCount: resolvedForms.length,
230
+ userAdmin: Boolean(user.admin),
231
+ userId: user.id,
232
+ });
233
+ return [3 /*break*/, 4];
234
+ case 3:
235
+ resolvedForms = activeForms;
236
+ _d.label = 4;
237
+ case 4: return [4 /*yield*/, consentResponse_1.ConsentResponse.find({ userId: user.id })];
238
+ case 5:
239
+ existingResponses = _d.sent();
240
+ respondedFormVersions = new Map();
241
+ try {
242
+ for (existingResponses_1 = __values(existingResponses), existingResponses_1_1 = existingResponses_1.next(); !existingResponses_1_1.done; existingResponses_1_1 = existingResponses_1.next()) {
243
+ response = existingResponses_1_1.value;
244
+ formId = response.consentFormId.toString();
245
+ respondedFormVersions.set(formId, (_c = response.formVersionSnapshot) !== null && _c !== void 0 ? _c : 0);
246
+ }
247
+ }
248
+ catch (e_1_1) { e_1 = { error: e_1_1 }; }
249
+ finally {
250
+ try {
251
+ if (existingResponses_1_1 && !existingResponses_1_1.done && (_a = existingResponses_1.return)) _a.call(existingResponses_1);
252
+ }
253
+ finally { if (e_1) throw e_1.error; }
254
+ }
255
+ respondedFormIds = existingResponses.map(function (r) { return r.consentFormId; });
256
+ return [4 /*yield*/, consentForm_1.ConsentForm.find({ _id: { $in: respondedFormIds } })];
257
+ case 6:
258
+ respondedForms = _d.sent();
259
+ formVersionByFormId = new Map();
260
+ try {
261
+ for (respondedForms_1 = __values(respondedForms), respondedForms_1_1 = respondedForms_1.next(); !respondedForms_1_1.done; respondedForms_1_1 = respondedForms_1.next()) {
262
+ form = respondedForms_1_1.value;
263
+ formVersionByFormId.set(form._id.toString(), form.version);
264
+ }
265
+ }
266
+ catch (e_2_1) { e_2 = { error: e_2_1 }; }
267
+ finally {
268
+ try {
269
+ if (respondedForms_1_1 && !respondedForms_1_1.done && (_b = respondedForms_1.return)) _b.call(respondedForms_1);
270
+ }
271
+ finally { if (e_2) throw e_2.error; }
272
+ }
273
+ pendingForms = resolvedForms.filter(function (form) {
274
+ var formId = form._id.toString();
275
+ // Find responses for this form
276
+ var matchingResponses = existingResponses.filter(function (r) { return r.consentFormId.toString() === formId; });
277
+ if (matchingResponses.length === 0) {
278
+ return true;
279
+ }
280
+ // Check if any response matches the current form version
281
+ return !matchingResponses.some(function (r) { return r.formVersionSnapshot === form.version; });
282
+ });
283
+ filteredOutByResolverCount = Math.max(activeForms.length - resolvedForms.length, 0);
284
+ filteredOutByResponsesCount = Math.max(resolvedForms.length - pendingForms.length, 0);
285
+ logger_1.logger.info("Pending consent forms fetched", {
286
+ activeFormCount: activeForms.length,
287
+ filteredOutByResolverCount: filteredOutByResolverCount,
288
+ filteredOutByResponsesCount: filteredOutByResponsesCount,
289
+ pendingFormCount: pendingForms.length,
290
+ resolvedFormCount: resolvedForms.length,
291
+ responseCount: existingResponses.length,
292
+ userAdmin: Boolean(user.admin),
293
+ userId: user.id,
294
+ });
295
+ return [2 /*return*/, res.json({ data: pendingForms })];
296
+ }
297
+ });
298
+ }); }));
299
+ // POST /consents/respond - submit a consent response
300
+ router.post("/respond", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
301
+ var user, _a, agreed, checkboxValues, consentFormId, locale, signature, form, values, i, checkbox, responseData, response;
302
+ var _b;
303
+ return __generator(this, function (_c) {
304
+ switch (_c.label) {
305
+ case 0:
306
+ user = req.user;
307
+ if (!user) {
308
+ throw new errors_1.APIError({ status: 401, title: "Authentication required" });
309
+ }
310
+ _a = req.body, agreed = _a.agreed, checkboxValues = _a.checkboxValues, consentFormId = _a.consentFormId, locale = _a.locale, signature = _a.signature;
311
+ if (!consentFormId) {
312
+ throw new errors_1.APIError({ status: 400, title: "consentFormId is required" });
313
+ }
314
+ if (agreed === undefined || agreed === null) {
315
+ throw new errors_1.APIError({ status: 400, title: "agreed field is required" });
316
+ }
317
+ if (!locale) {
318
+ throw new errors_1.APIError({ status: 400, title: "locale is required" });
319
+ }
320
+ return [4 /*yield*/, consentForm_1.ConsentForm.findExactlyOne({ _id: consentFormId }, { status: 404, title: "Consent form not found" })];
321
+ case 1:
322
+ form = _c.sent();
323
+ if (!form.active) {
324
+ throw new errors_1.APIError({ status: 400, title: "Consent form is not active" });
325
+ }
326
+ // Validate signature requirement
327
+ if (form.captureSignature && agreed && !signature) {
328
+ throw new errors_1.APIError({
329
+ status: 400,
330
+ title: "Signature is required for this consent form",
331
+ });
332
+ }
333
+ // Validate required checkboxes
334
+ if (agreed && form.checkboxes.length > 0) {
335
+ values = checkboxValues !== null && checkboxValues !== void 0 ? checkboxValues : {};
336
+ for (i = 0; i < form.checkboxes.length; i++) {
337
+ checkbox = form.checkboxes[i];
338
+ if (checkbox.required && !values[i.toString()]) {
339
+ throw new errors_1.APIError({
340
+ status: 400,
341
+ title: "Required checkbox \"".concat(checkbox.label, "\" must be checked"),
342
+ });
343
+ }
344
+ }
345
+ }
346
+ responseData = {
347
+ agreed: agreed,
348
+ agreedAt: luxon_1.DateTime.now().toJSDate(),
349
+ consentFormId: form._id,
350
+ locale: locale,
351
+ userId: user.id,
352
+ };
353
+ if (checkboxValues !== undefined) {
354
+ responseData.checkboxValues = checkboxValues;
355
+ }
356
+ if (signature) {
357
+ responseData.signature = signature;
358
+ responseData.signedAt = luxon_1.DateTime.now().toJSDate();
359
+ }
360
+ if (auditTrail) {
361
+ responseData.ipAddress = req.ip;
362
+ responseData.userAgent = req.headers["user-agent"];
363
+ responseData.contentSnapshot =
364
+ (_b = form.content.get(locale)) !== null && _b !== void 0 ? _b : form.content.get(form.defaultLocale);
365
+ responseData.formVersionSnapshot = form.version;
366
+ }
367
+ else {
368
+ responseData.formVersionSnapshot = form.version;
369
+ }
370
+ return [4 /*yield*/, consentResponse_1.ConsentResponse.create(responseData)];
371
+ case 2:
372
+ response = _c.sent();
373
+ logger_1.logger.info("Consent response recorded", {
374
+ agreed: agreed,
375
+ consentFormId: form._id.toString(),
376
+ locale: locale,
377
+ userId: user.id,
378
+ });
379
+ return [2 /*return*/, res.json({ data: response })];
380
+ }
381
+ });
382
+ }); }));
383
+ // GET /consents/my - fetch the current user's consent responses with form data
384
+ router.get("/my", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
385
+ var user, responses, formIds, forms, formMap, data;
386
+ return __generator(this, function (_a) {
387
+ switch (_a.label) {
388
+ case 0:
389
+ user = req.user;
390
+ if (!user) {
391
+ throw new errors_1.APIError({ status: 401, title: "Authentication required" });
392
+ }
393
+ return [4 /*yield*/, consentResponse_1.ConsentResponse.find({ userId: user.id }).sort({ agreedAt: -1 })];
394
+ case 1:
395
+ responses = _a.sent();
396
+ formIds = responses.map(function (r) { return r.consentFormId; });
397
+ return [4 /*yield*/, consentForm_1.ConsentForm.find({ _id: { $in: formIds } })];
398
+ case 2:
399
+ forms = _a.sent();
400
+ formMap = new Map(forms.map(function (f) { return [f._id.toString(), f]; }));
401
+ data = responses.map(function (response) {
402
+ var form = formMap.get(response.consentFormId.toString());
403
+ return {
404
+ _id: response._id,
405
+ agreed: response.agreed,
406
+ agreedAt: response.agreedAt,
407
+ checkboxValues: response.checkboxValues,
408
+ contentSnapshot: response.contentSnapshot,
409
+ form: form
410
+ ? {
411
+ captureSignature: form.captureSignature,
412
+ checkboxes: form.checkboxes,
413
+ slug: form.slug,
414
+ title: form.title,
415
+ type: form.type,
416
+ version: form.version,
417
+ }
418
+ : null,
419
+ formVersionSnapshot: response.formVersionSnapshot,
420
+ ipAddress: response.ipAddress,
421
+ locale: response.locale,
422
+ signature: response.signature,
423
+ signedAt: response.signedAt,
424
+ userAgent: response.userAgent,
425
+ };
426
+ });
427
+ return [2 /*return*/, res.json({ data: data })];
428
+ }
429
+ });
430
+ }); }));
431
+ // GET /consents/audit/:userId - admin audit trail for a specific user
432
+ if (auditTrail) {
433
+ router.get("/audit/:userId", (0, auth_1.authenticateMiddleware)(), (0, api_1.asyncHandler)(function (req, res) { return __awaiter(_this, void 0, void 0, function () {
434
+ var user, responses, formIds, forms, formMap, auditEntries;
435
+ return __generator(this, function (_a) {
436
+ switch (_a.label) {
437
+ case 0:
438
+ user = req.user;
439
+ if (!(user === null || user === void 0 ? void 0 : user.admin)) {
440
+ throw new errors_1.APIError({ status: 403, title: "Admin access required" });
441
+ }
442
+ return [4 /*yield*/, consentResponse_1.ConsentResponse.find({ userId: req.params.userId }).sort({
443
+ agreedAt: -1,
444
+ })];
445
+ case 1:
446
+ responses = _a.sent();
447
+ formIds = responses.map(function (r) { return r.consentFormId; });
448
+ return [4 /*yield*/, consentForm_1.ConsentForm.find({ _id: { $in: formIds } })];
449
+ case 2:
450
+ forms = _a.sent();
451
+ formMap = new Map(forms.map(function (f) { return [f._id.toString(), f]; }));
452
+ auditEntries = responses.map(function (response) {
453
+ var form = formMap.get(response.consentFormId.toString());
454
+ return {
455
+ agreed: response.agreed,
456
+ agreedAt: response.agreedAt,
457
+ contentSnapshot: response.contentSnapshot,
458
+ form: form
459
+ ? {
460
+ slug: form.slug,
461
+ title: form.title,
462
+ type: form.type,
463
+ version: form.version,
464
+ }
465
+ : null,
466
+ formVersionSnapshot: response.formVersionSnapshot,
467
+ ipAddress: response.ipAddress,
468
+ locale: response.locale,
469
+ responseId: response._id,
470
+ signedAt: response.signedAt,
471
+ userAgent: response.userAgent,
472
+ };
473
+ });
474
+ return [2 /*return*/, res.json({ data: auditEntries })];
475
+ }
476
+ });
477
+ }); }));
478
+ }
479
+ app.use("/consents", router);
480
+ logger_1.logger.info("ConsentApp registered", { auditTrail: Boolean(auditTrail) });
481
+ };
482
+ return ConsentApp;
483
+ }());
484
+ exports.ConsentApp = ConsentApp;
@@ -0,0 +1 @@
1
+ export {};