@terreno/api 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/{versionCheck.test.js → versionCheckPlugin.test.js} +2 -2
- package/dist/api.d.ts +4 -2
- package/dist/api.js +7 -2
- package/dist/consentApp.d.ts +33 -0
- package/dist/consentApp.js +484 -0
- package/dist/consentApp.test.d.ts +1 -0
- package/dist/consentApp.test.js +1132 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/models/consentForm.d.ts +2 -0
- package/dist/models/consentForm.js +115 -0
- package/dist/models/consentResponse.d.ts +2 -0
- package/dist/models/consentResponse.js +73 -0
- package/dist/models/versionConfig.d.ts +1 -1
- package/dist/openApiValidator.js +2 -0
- package/dist/populate.d.ts +1 -0
- package/dist/populate.js +53 -13
- package/dist/syncConsents.d.ts +67 -0
- package/dist/syncConsents.js +334 -0
- package/dist/syncConsents.test.d.ts +1 -0
- package/dist/syncConsents.test.js +249 -0
- package/dist/terrenoApp.js +6 -5
- package/dist/terrenoPlugin.d.ts +1 -1
- package/dist/types/consentForm.d.ts +32 -0
- package/dist/types/consentForm.js +2 -0
- package/dist/types/consentResponse.d.ts +23 -0
- package/dist/types/consentResponse.js +2 -0
- package/dist/vendor/wesleytodd-openapi/lib/generate-doc.js +9 -2
- package/dist/versionCheckPlugin.d.ts +2 -0
- package/dist/versionCheckPlugin.js +3 -6
- package/package.json +1 -1
- package/src/__tests__/{versionCheck.test.ts → versionCheckPlugin.test.ts} +2 -2
- package/src/api.ts +11 -4
- package/src/consentApp.test.ts +749 -0
- package/src/consentApp.ts +463 -0
- package/src/index.ts +6 -0
- package/src/models/consentForm.ts +123 -0
- package/src/models/consentResponse.ts +78 -0
- package/src/models/versionConfig.ts +1 -1
- package/src/openApiValidator.ts +2 -0
- package/src/populate.ts +33 -0
- package/src/syncConsents.test.ts +124 -0
- package/src/syncConsents.ts +263 -0
- package/src/terrenoApp.ts +6 -6
- package/src/terrenoPlugin.ts +1 -1
- package/src/types/consentForm.ts +41 -0
- package/src/types/consentResponse.ts +34 -0
- package/src/vendor/wesleytodd-openapi/lib/generate-doc.js +8 -2
- package/src/versionCheckPlugin.ts +5 -6
- /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 {
|
|
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 {};
|