@primocaredentgroup/convex-campaigns-component 0.3.5 → 0.3.9
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/convex/components/campaigns/functions/mutations.ts +14 -6
- package/convex/components/campaigns/functions/playground.ts +27 -16
- package/convex/components/campaigns/functions/public.ts +12 -7
- package/convex/components/campaigns/functions/queries.ts +11 -7
- package/convex/components/campaigns/permissions.ts +23 -8
- package/convex.config.ts +2 -10
- package/package.json +6 -2
|
@@ -34,8 +34,11 @@ function normalizeScopeClinicIds(
|
|
|
34
34
|
return [...new Set(scopeClinicIds.map((id) => String(id).trim()).filter(Boolean))];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
const trustedUserIdArg = { _trustedUserId: v.optional(v.string()) };
|
|
38
|
+
|
|
37
39
|
export const createCampaign = mutation({
|
|
38
40
|
args: {
|
|
41
|
+
...trustedUserIdArg,
|
|
39
42
|
input: v.object({
|
|
40
43
|
name: v.string(),
|
|
41
44
|
description: v.optional(v.string()),
|
|
@@ -47,7 +50,7 @@ export const createCampaign = mutation({
|
|
|
47
50
|
}),
|
|
48
51
|
},
|
|
49
52
|
handler: async (ctx, args) => {
|
|
50
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
53
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
51
54
|
const now = Date.now();
|
|
52
55
|
return ctx.db.insert("campaigns", {
|
|
53
56
|
...args.input,
|
|
@@ -62,6 +65,7 @@ export const createCampaign = mutation({
|
|
|
62
65
|
|
|
63
66
|
export const updateCampaign = mutation({
|
|
64
67
|
args: {
|
|
68
|
+
...trustedUserIdArg,
|
|
65
69
|
campaignId: v.id("campaigns"),
|
|
66
70
|
patch: v.object({
|
|
67
71
|
name: v.optional(v.string()),
|
|
@@ -74,7 +78,7 @@ export const updateCampaign = mutation({
|
|
|
74
78
|
}),
|
|
75
79
|
},
|
|
76
80
|
handler: async (ctx, args) => {
|
|
77
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
81
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
78
82
|
const campaign = await ctx.db.get(args.campaignId);
|
|
79
83
|
if (!campaign) throw new Error("Campaign not found");
|
|
80
84
|
await ctx.db.patch(args.campaignId, {
|
|
@@ -93,11 +97,12 @@ export const updateCampaign = mutation({
|
|
|
93
97
|
|
|
94
98
|
export const setCampaignStatus = mutation({
|
|
95
99
|
args: {
|
|
100
|
+
...trustedUserIdArg,
|
|
96
101
|
campaignId: v.id("campaigns"),
|
|
97
102
|
status: campaignStatusValidator,
|
|
98
103
|
},
|
|
99
104
|
handler: async (ctx, args) => {
|
|
100
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
105
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
101
106
|
await ctx.db.patch(args.campaignId, { status: args.status, updatedAt: Date.now() });
|
|
102
107
|
await ctx.db.insert("event_log", {
|
|
103
108
|
campaignId: args.campaignId,
|
|
@@ -111,6 +116,7 @@ export const setCampaignStatus = mutation({
|
|
|
111
116
|
|
|
112
117
|
export const addOrUpdateSteps = mutation({
|
|
113
118
|
args: {
|
|
119
|
+
...trustedUserIdArg,
|
|
114
120
|
campaignId: v.id("campaigns"),
|
|
115
121
|
steps: v.array(
|
|
116
122
|
v.object({
|
|
@@ -123,7 +129,7 @@ export const addOrUpdateSteps = mutation({
|
|
|
123
129
|
),
|
|
124
130
|
},
|
|
125
131
|
handler: async (ctx, args) => {
|
|
126
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
132
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
127
133
|
const now = Date.now();
|
|
128
134
|
const createdOrUpdated: string[] = [];
|
|
129
135
|
for (const step of args.steps) {
|
|
@@ -155,12 +161,13 @@ export const addOrUpdateSteps = mutation({
|
|
|
155
161
|
|
|
156
162
|
export const previewAudience = mutation({
|
|
157
163
|
args: {
|
|
164
|
+
...trustedUserIdArg,
|
|
158
165
|
campaignId: v.id("campaigns"),
|
|
159
166
|
segmentationRules: segmentationRulesValidator,
|
|
160
167
|
sampleSize: v.optional(v.number()),
|
|
161
168
|
},
|
|
162
169
|
handler: async (ctx, args) => {
|
|
163
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
170
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
164
171
|
const campaign = await ctx.db.get(args.campaignId);
|
|
165
172
|
if (!campaign) throw new Error("Campaign not found");
|
|
166
173
|
|
|
@@ -229,12 +236,13 @@ export const previewAudience = mutation({
|
|
|
229
236
|
|
|
230
237
|
export const buildAudienceSnapshot = mutation({
|
|
231
238
|
args: {
|
|
239
|
+
...trustedUserIdArg,
|
|
232
240
|
campaignId: v.id("campaigns"),
|
|
233
241
|
segmentationRules: segmentationRulesValidator,
|
|
234
242
|
runConfig: v.optional(runConfigValidator),
|
|
235
243
|
},
|
|
236
244
|
handler: async (ctx, args) => {
|
|
237
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
245
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
238
246
|
const campaign = await ctx.db.get(args.campaignId);
|
|
239
247
|
if (!campaign) throw new Error("Campaign not found");
|
|
240
248
|
|
|
@@ -11,6 +11,7 @@ import type { RuleGroup } from "../v2/types";
|
|
|
11
11
|
import { DEFAULT_CALL_POLICY } from "../domain/types";
|
|
12
12
|
|
|
13
13
|
const MAX_PREVIEW = 5000;
|
|
14
|
+
const trustedUserIdArg = { _trustedUserId: v.optional(v.string()) };
|
|
14
15
|
|
|
15
16
|
function hashRules(rules: unknown): string {
|
|
16
17
|
const text = JSON.stringify(rules);
|
|
@@ -81,12 +82,13 @@ export const getFieldRegistry = query({
|
|
|
81
82
|
|
|
82
83
|
export const seedDemoData = mutation({
|
|
83
84
|
args: {
|
|
85
|
+
...trustedUserIdArg,
|
|
84
86
|
orgId: v.string(),
|
|
85
87
|
clinicIds: v.array(v.string()),
|
|
86
88
|
nPatients: v.number(),
|
|
87
89
|
},
|
|
88
90
|
handler: async (ctx, args) => {
|
|
89
|
-
await assertCanManageCampaigns(ctx);
|
|
91
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
90
92
|
const clinicIds = normalizeClinicIds(args.clinicIds);
|
|
91
93
|
if (clinicIds.length === 0) throw new Error("Almeno una clinicId è obbligatoria.");
|
|
92
94
|
|
|
@@ -185,11 +187,12 @@ export const seedDemoData = mutation({
|
|
|
185
187
|
|
|
186
188
|
export const updateCampaignCallPolicy = mutation({
|
|
187
189
|
args: {
|
|
190
|
+
...trustedUserIdArg,
|
|
188
191
|
campaignId: v.id("campaigns"),
|
|
189
192
|
callPolicy: v.any(),
|
|
190
193
|
},
|
|
191
194
|
handler: async (ctx, args) => {
|
|
192
|
-
await assertCanManageCampaigns(ctx);
|
|
195
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
193
196
|
const validation = validateCallPolicyInput(args.callPolicy);
|
|
194
197
|
if (!validation.ok) {
|
|
195
198
|
throw new Error(`CALL_POLICY_VALIDATION_ERROR: ${validation.errors.join(" | ")}`);
|
|
@@ -215,10 +218,10 @@ export const updateCampaignCallPolicy = mutation({
|
|
|
215
218
|
});
|
|
216
219
|
|
|
217
220
|
export const getCampaignCallPolicy = query({
|
|
218
|
-
args: { campaignId: v.id("campaigns") },
|
|
221
|
+
args: { ...trustedUserIdArg, campaignId: v.id("campaigns") },
|
|
219
222
|
handler: async (ctx, args) => {
|
|
220
223
|
try {
|
|
221
|
-
await assertCanManageCampaigns(ctx);
|
|
224
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
222
225
|
const campaign = await ctx.db.get(args.campaignId);
|
|
223
226
|
if (!campaign) return null;
|
|
224
227
|
const policy = campaign.callPolicy ?? getDefaultCallPolicy();
|
|
@@ -232,6 +235,7 @@ export const getCampaignCallPolicy = query({
|
|
|
232
235
|
|
|
233
236
|
export const upsertCampaign = mutation({
|
|
234
237
|
args: {
|
|
238
|
+
...trustedUserIdArg,
|
|
235
239
|
id: v.optional(v.id("campaigns")),
|
|
236
240
|
orgId: v.string(),
|
|
237
241
|
name: v.string(),
|
|
@@ -243,7 +247,7 @@ export const upsertCampaign = mutation({
|
|
|
243
247
|
frequencyCapDays: v.optional(v.number()),
|
|
244
248
|
},
|
|
245
249
|
handler: async (ctx, args) => {
|
|
246
|
-
await assertCanManageCampaigns(ctx);
|
|
250
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
247
251
|
|
|
248
252
|
const rulesValidation = validateRuleDsl(args.rules);
|
|
249
253
|
if (!rulesValidation.ok) {
|
|
@@ -297,11 +301,12 @@ export const upsertCampaign = mutation({
|
|
|
297
301
|
|
|
298
302
|
export const setCampaignLifecycleStatus = mutation({
|
|
299
303
|
args: {
|
|
304
|
+
...trustedUserIdArg,
|
|
300
305
|
campaignId: v.id("campaigns"),
|
|
301
306
|
status: v.union(v.literal("draft"), v.literal("ready"), v.literal("archived")),
|
|
302
307
|
},
|
|
303
308
|
handler: async (ctx, args) => {
|
|
304
|
-
await assertCanManageCampaigns(ctx);
|
|
309
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
305
310
|
const campaign = await ctx.db.get(args.campaignId);
|
|
306
311
|
if (!campaign) throw new Error("Campaign non trovata.");
|
|
307
312
|
const statusToV2: Record<string, "draft" | "ready" | "archived"> = {
|
|
@@ -333,12 +338,13 @@ export const setCampaignLifecycleStatus = mutation({
|
|
|
333
338
|
|
|
334
339
|
export const listCampaignsV2 = query({
|
|
335
340
|
args: {
|
|
341
|
+
...trustedUserIdArg,
|
|
336
342
|
orgId: v.string(),
|
|
337
343
|
status: v.optional(v.union(v.literal("draft"), v.literal("ready"), v.literal("archived"))),
|
|
338
344
|
},
|
|
339
345
|
handler: async (ctx, args) => {
|
|
340
346
|
try {
|
|
341
|
-
await assertCanManageCampaigns(ctx);
|
|
347
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
342
348
|
const rows = args.status
|
|
343
349
|
? await safeCampaignsByOrgStatus(ctx, args.orgId, args.status)
|
|
344
350
|
: await safeCampaignsByOrg(ctx, args.orgId);
|
|
@@ -351,10 +357,10 @@ export const listCampaignsV2 = query({
|
|
|
351
357
|
});
|
|
352
358
|
|
|
353
359
|
export const getCampaignV2 = query({
|
|
354
|
-
args: { id: v.id("campaigns") },
|
|
360
|
+
args: { ...trustedUserIdArg, id: v.id("campaigns") },
|
|
355
361
|
handler: async (ctx, args) => {
|
|
356
362
|
try {
|
|
357
|
-
await assertCanManageCampaigns(ctx);
|
|
363
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
358
364
|
return await ctx.db.get(args.id);
|
|
359
365
|
} catch (error) {
|
|
360
366
|
console.error("[getCampaignV2]", error);
|
|
@@ -364,10 +370,10 @@ export const getCampaignV2 = query({
|
|
|
364
370
|
});
|
|
365
371
|
|
|
366
372
|
export const listCampaignVersions = query({
|
|
367
|
-
args: { campaignId: v.id("campaigns") },
|
|
373
|
+
args: { ...trustedUserIdArg, campaignId: v.id("campaigns") },
|
|
368
374
|
handler: async (ctx, args) => {
|
|
369
375
|
try {
|
|
370
|
-
await assertCanManageCampaigns(ctx);
|
|
376
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
371
377
|
const versions = await ctx.db
|
|
372
378
|
.query("campaignVersions")
|
|
373
379
|
.withIndex("by_campaign", (q: any) => q.eq("campaignId", args.campaignId))
|
|
@@ -382,6 +388,7 @@ export const listCampaignVersions = query({
|
|
|
382
388
|
|
|
383
389
|
export const previewCampaignAudience = query({
|
|
384
390
|
args: {
|
|
391
|
+
...trustedUserIdArg,
|
|
385
392
|
campaignId: v.optional(v.id("campaigns")),
|
|
386
393
|
orgId: v.optional(v.string()),
|
|
387
394
|
clinicIds: v.optional(v.array(v.string())),
|
|
@@ -389,7 +396,7 @@ export const previewCampaignAudience = query({
|
|
|
389
396
|
frequencyCapDays: v.optional(v.number()),
|
|
390
397
|
},
|
|
391
398
|
handler: async (ctx, args) => {
|
|
392
|
-
await assertCanManageCampaigns(ctx);
|
|
399
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
393
400
|
|
|
394
401
|
let orgId = args.orgId ?? "";
|
|
395
402
|
let clinicIds = normalizeClinicIds(args.clinicIds);
|
|
@@ -470,11 +477,12 @@ export const previewCampaignAudience = query({
|
|
|
470
477
|
|
|
471
478
|
export const publishCampaignVersion = mutation({
|
|
472
479
|
args: {
|
|
480
|
+
...trustedUserIdArg,
|
|
473
481
|
campaignId: v.id("campaigns"),
|
|
474
482
|
label: v.optional(v.string()),
|
|
475
483
|
},
|
|
476
484
|
handler: async (ctx, args) => {
|
|
477
|
-
await assertCanManageCampaigns(ctx);
|
|
485
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
478
486
|
const campaign = await ctx.db.get(args.campaignId);
|
|
479
487
|
if (!campaign) throw new Error("Campaign non trovata.");
|
|
480
488
|
if (campaign.status !== "ready") {
|
|
@@ -588,12 +596,13 @@ async function safeAudienceMembersByOrgPatient(ctx: any, orgId: string, patientI
|
|
|
588
596
|
|
|
589
597
|
export const getPatientCampaignMemberships = query({
|
|
590
598
|
args: {
|
|
599
|
+
...trustedUserIdArg,
|
|
591
600
|
orgId: v.string(),
|
|
592
601
|
patientId: v.string(),
|
|
593
602
|
},
|
|
594
603
|
handler: async (ctx, args) => {
|
|
595
604
|
try {
|
|
596
|
-
await assertCanManageCampaigns(ctx);
|
|
605
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
597
606
|
const memberships = await safeAudienceMembersByOrgPatient(
|
|
598
607
|
ctx,
|
|
599
608
|
args.orgId,
|
|
@@ -665,13 +674,14 @@ export const getPatientCampaignMemberships = query({
|
|
|
665
674
|
|
|
666
675
|
export const getAudiencePage = query({
|
|
667
676
|
args: {
|
|
677
|
+
...trustedUserIdArg,
|
|
668
678
|
versionId: v.id("campaignVersions"),
|
|
669
679
|
clinicId: v.optional(v.string()),
|
|
670
680
|
cursor: v.optional(v.string()),
|
|
671
681
|
limit: v.optional(v.number()),
|
|
672
682
|
},
|
|
673
683
|
handler: async (ctx, args) => {
|
|
674
|
-
await assertCanManageCampaigns(ctx);
|
|
684
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
675
685
|
const limit = Math.min(200, Math.max(1, args.limit ?? 50));
|
|
676
686
|
if (args.clinicId) {
|
|
677
687
|
const page = await ctx.db
|
|
@@ -701,6 +711,7 @@ export const getAudiencePage = query({
|
|
|
701
711
|
|
|
702
712
|
export const recordContactAttempt = mutation({
|
|
703
713
|
args: {
|
|
714
|
+
...trustedUserIdArg,
|
|
704
715
|
orgId: v.string(),
|
|
705
716
|
clinicId: v.string(),
|
|
706
717
|
patientId: v.string(),
|
|
@@ -718,7 +729,7 @@ export const recordContactAttempt = mutation({
|
|
|
718
729
|
),
|
|
719
730
|
},
|
|
720
731
|
handler: async (ctx, args) => {
|
|
721
|
-
await assertCanManageCampaigns(ctx);
|
|
732
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
722
733
|
const id = await ctx.db.insert("contactLog", {
|
|
723
734
|
...args,
|
|
724
735
|
createdAt: Date.now(),
|
|
@@ -10,6 +10,8 @@ import { getDefaultCallPolicy } from "../v2/callPolicy";
|
|
|
10
10
|
import { normalizeClinicId, normalizeClinicIds } from "../lib/helpers";
|
|
11
11
|
import type { Id } from "../../../_generated/dataModel";
|
|
12
12
|
|
|
13
|
+
const trustedUserIdArg = { _trustedUserId: v.optional(v.string()) };
|
|
14
|
+
|
|
13
15
|
async function safeCampaignsByOrgStatus(ctx: any, orgId: string, status: string) {
|
|
14
16
|
try {
|
|
15
17
|
return await ctx.db
|
|
@@ -56,11 +58,12 @@ async function safeAudienceMembersByOrgPatient(ctx: any, orgId: string, patientI
|
|
|
56
58
|
|
|
57
59
|
export const listActiveCampaignsForOrg = query({
|
|
58
60
|
args: {
|
|
61
|
+
...trustedUserIdArg,
|
|
59
62
|
orgId: v.string(),
|
|
60
63
|
clinicId: v.optional(v.string()),
|
|
61
64
|
},
|
|
62
65
|
handler: async (ctx, args) => {
|
|
63
|
-
await assertCanManageCampaigns(ctx);
|
|
66
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
64
67
|
const campaigns = await safeCampaignsByOrgStatus(ctx, args.orgId, "ready");
|
|
65
68
|
const clinicIdNorm = args.clinicId ? normalizeClinicId(args.clinicId) : null;
|
|
66
69
|
|
|
@@ -107,9 +110,9 @@ export const listActiveCampaignsForOrg = query({
|
|
|
107
110
|
});
|
|
108
111
|
|
|
109
112
|
export const getLatestPublishedVersion = query({
|
|
110
|
-
args: { campaignId: v.id("campaigns") },
|
|
113
|
+
args: { ...trustedUserIdArg, campaignId: v.id("campaigns") },
|
|
111
114
|
handler: async (ctx, args) => {
|
|
112
|
-
await assertCanManageCampaigns(ctx);
|
|
115
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
113
116
|
const versions = await ctx.db
|
|
114
117
|
.query("campaignVersions")
|
|
115
118
|
.withIndex("by_campaign", (q: any) => q.eq("campaignId", args.campaignId))
|
|
@@ -128,9 +131,9 @@ export const getLatestPublishedVersion = query({
|
|
|
128
131
|
});
|
|
129
132
|
|
|
130
133
|
export const getPatientCampaignMemberships = query({
|
|
131
|
-
args: { orgId: v.string(), patientId: v.string() },
|
|
134
|
+
args: { ...trustedUserIdArg, orgId: v.string(), patientId: v.string() },
|
|
132
135
|
handler: async (ctx, args) => {
|
|
133
|
-
await assertCanManageCampaigns(ctx);
|
|
136
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
134
137
|
const memberships = await safeAudienceMembersByOrgPatient(
|
|
135
138
|
ctx,
|
|
136
139
|
args.orgId,
|
|
@@ -184,13 +187,14 @@ export const getPatientCampaignMemberships = query({
|
|
|
184
187
|
|
|
185
188
|
export const getAudiencePage = query({
|
|
186
189
|
args: {
|
|
190
|
+
...trustedUserIdArg,
|
|
187
191
|
versionId: v.id("campaignVersions"),
|
|
188
192
|
clinicId: v.optional(v.string()),
|
|
189
193
|
cursor: v.optional(v.string()),
|
|
190
194
|
limit: v.optional(v.number()),
|
|
191
195
|
},
|
|
192
196
|
handler: async (ctx, args) => {
|
|
193
|
-
await assertCanManageCampaigns(ctx);
|
|
197
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
194
198
|
const limit = Math.min(200, Math.max(1, args.limit ?? 50));
|
|
195
199
|
const clinicIdNorm = args.clinicId ? normalizeClinicId(args.clinicId) : undefined;
|
|
196
200
|
|
|
@@ -214,6 +218,7 @@ export const getAudiencePage = query({
|
|
|
214
218
|
|
|
215
219
|
export const recordContactAttempt = mutation({
|
|
216
220
|
args: {
|
|
221
|
+
...trustedUserIdArg,
|
|
217
222
|
orgId: v.string(),
|
|
218
223
|
clinicId: v.string(),
|
|
219
224
|
patientId: v.string(),
|
|
@@ -231,7 +236,7 @@ export const recordContactAttempt = mutation({
|
|
|
231
236
|
),
|
|
232
237
|
},
|
|
233
238
|
handler: async (ctx, args) => {
|
|
234
|
-
await assertCanManageCampaigns(ctx);
|
|
239
|
+
await assertCanManageCampaigns(ctx, { trustedUserId: args._trustedUserId });
|
|
235
240
|
const clinicIdNorm = normalizeClinicId(args.clinicId);
|
|
236
241
|
const id = await ctx.db.insert("contactLog", {
|
|
237
242
|
...args,
|
|
@@ -4,6 +4,7 @@ import { assertAuthorized } from "../permissions";
|
|
|
4
4
|
import { campaignStatusValidator, scopeTypeValidator } from "../domain/types";
|
|
5
5
|
|
|
6
6
|
const clinicIdFilterValidator = v.union(v.id("clinics"), v.string());
|
|
7
|
+
const trustedUserIdArg = { _trustedUserId: v.optional(v.string()) };
|
|
7
8
|
|
|
8
9
|
function isRecoverableQueryError(error: unknown): boolean {
|
|
9
10
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -72,6 +73,7 @@ async function safeCollectMembersBySnapshot(ctx: any, snapshotId: string) {
|
|
|
72
73
|
|
|
73
74
|
export const listCampaigns = query({
|
|
74
75
|
args: {
|
|
76
|
+
...trustedUserIdArg,
|
|
75
77
|
filters: v.optional(
|
|
76
78
|
v.object({
|
|
77
79
|
status: v.optional(campaignStatusValidator),
|
|
@@ -82,7 +84,7 @@ export const listCampaigns = query({
|
|
|
82
84
|
},
|
|
83
85
|
handler: async (ctx, args) => {
|
|
84
86
|
try {
|
|
85
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
87
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
86
88
|
let campaigns = args.filters?.status
|
|
87
89
|
? await safeCollectCampaignsByStatus(ctx, args.filters.status)
|
|
88
90
|
: await ctx.db.query("campaigns").collect();
|
|
@@ -105,10 +107,10 @@ export const listCampaigns = query({
|
|
|
105
107
|
});
|
|
106
108
|
|
|
107
109
|
export const getCampaign = query({
|
|
108
|
-
args: { campaignId: v.id("campaigns") },
|
|
110
|
+
args: { ...trustedUserIdArg, campaignId: v.id("campaigns") },
|
|
109
111
|
handler: async (ctx, args) => {
|
|
110
112
|
try {
|
|
111
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
113
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
112
114
|
const campaign = await ctx.db.get(args.campaignId);
|
|
113
115
|
if (!campaign) return null;
|
|
114
116
|
|
|
@@ -129,10 +131,10 @@ export const getCampaign = query({
|
|
|
129
131
|
});
|
|
130
132
|
|
|
131
133
|
export const getSnapshot = query({
|
|
132
|
-
args: { snapshotId: v.id("audience_snapshots") },
|
|
134
|
+
args: { ...trustedUserIdArg, snapshotId: v.id("audience_snapshots") },
|
|
133
135
|
handler: async (ctx, args) => {
|
|
134
136
|
try {
|
|
135
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
137
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
136
138
|
const snapshot = await ctx.db.get(args.snapshotId);
|
|
137
139
|
if (!snapshot) return null;
|
|
138
140
|
|
|
@@ -152,12 +154,13 @@ export const getSnapshot = query({
|
|
|
152
154
|
|
|
153
155
|
export const getSnapshotMemberSample = query({
|
|
154
156
|
args: {
|
|
157
|
+
...trustedUserIdArg,
|
|
155
158
|
snapshotId: v.id("audience_snapshots"),
|
|
156
159
|
limit: v.optional(v.number()),
|
|
157
160
|
},
|
|
158
161
|
handler: async (ctx, args) => {
|
|
159
162
|
try {
|
|
160
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
163
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
161
164
|
const limit = Math.min(Math.max(1, args.limit ?? 10), 100);
|
|
162
165
|
|
|
163
166
|
let members: any[];
|
|
@@ -220,6 +223,7 @@ export const getSnapshotMemberSample = query({
|
|
|
220
223
|
|
|
221
224
|
export const getCampaignReport = query({
|
|
222
225
|
args: {
|
|
226
|
+
...trustedUserIdArg,
|
|
223
227
|
campaignId: v.id("campaigns"),
|
|
224
228
|
range: v.object({
|
|
225
229
|
from: v.number(),
|
|
@@ -245,7 +249,7 @@ export const getCampaignReport = query({
|
|
|
245
249
|
};
|
|
246
250
|
|
|
247
251
|
try {
|
|
248
|
-
await assertAuthorized(ctx, ["Admin", "Marketing"]);
|
|
252
|
+
await assertAuthorized(ctx, ["Admin", "Marketing"], { trustedUserId: args._trustedUserId });
|
|
249
253
|
const snapshots = await safeCollectSnapshotsByCampaign(ctx, args.campaignId);
|
|
250
254
|
|
|
251
255
|
const inRange = snapshots.filter(
|
|
@@ -115,14 +115,25 @@ async function getUserRoleCodes(ctx: AnyCtx, userId: Id<"users">): Promise<Set<s
|
|
|
115
115
|
return roles;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
type AssertAuthOptions = {
|
|
119
|
+
trustedUserId?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
118
122
|
/**
|
|
119
123
|
* MVP authorization guard.
|
|
120
|
-
* -
|
|
121
|
-
* -
|
|
122
|
-
*
|
|
123
|
-
* TODO: tighten unauthenticated fallback once host auth is fully configured.
|
|
124
|
+
* - Se _trustedUserId è fornito dall'host: bypass completo (l'host ha già validato l'utente).
|
|
125
|
+
* - Se auth identity è disponibile: enforce dei ruoli richiesti.
|
|
126
|
+
* - Se auth identity manca: fallback per servizi in fase di wiring.
|
|
124
127
|
*/
|
|
125
|
-
export async function assertAuthorized(
|
|
128
|
+
export async function assertAuthorized(
|
|
129
|
+
ctx: AnyCtx,
|
|
130
|
+
requiredRoles: Role[],
|
|
131
|
+
options?: AssertAuthOptions,
|
|
132
|
+
): Promise<void> {
|
|
133
|
+
if (options?.trustedUserId) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
const identity = await ctx.auth.getUserIdentity();
|
|
127
138
|
if (!identity) {
|
|
128
139
|
if (requiredRoles.includes("System")) {
|
|
@@ -180,11 +191,15 @@ export async function assertAuthorized(ctx: AnyCtx, requiredRoles: Role[]): Prom
|
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
export async function assertCanManageCampaigns(
|
|
194
|
+
export async function assertCanManageCampaigns(
|
|
195
|
+
ctx: AnyCtx,
|
|
196
|
+
options?: AssertAuthOptions,
|
|
197
|
+
): Promise<void> {
|
|
198
|
+
if (options?.trustedUserId) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
184
201
|
const identity = await ctx.auth.getUserIdentity();
|
|
185
202
|
if (!identity) {
|
|
186
203
|
throw authError("CAMPAIGNS_UNAUTHORIZED", "Missing identity in request context.");
|
|
187
204
|
}
|
|
188
|
-
// Placeholder policy: any authenticated user can manage campaigns.
|
|
189
|
-
// TODO: replace with PrimoCore role checks (admin/marketing).
|
|
190
205
|
}
|
package/convex.config.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defineComponent } from "convex/server";
|
|
2
2
|
|
|
3
3
|
const campaignsComponent = defineComponent("campaigns");
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
// L'host che installa da npm usa: import campaignsComponent from "package/convex.config"
|
|
7
|
-
// e fa app.use(campaignsComponent) - quindi serve il componente.
|
|
8
|
-
const app = defineApp();
|
|
9
|
-
app.use(campaignsComponent, { name: "campaigns" });
|
|
10
|
-
|
|
11
|
-
export default app;
|
|
12
|
-
// Per host npm: import { campaignsComponent } from "package/convex.config"
|
|
13
|
-
export { campaignsComponent };
|
|
5
|
+
export default campaignsComponent;
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@primocaredentgroup/convex-campaigns-component",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Convex Campaigns backend component for PrimoCore",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "convex.config.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
"./convex.config.js": "./convex.config.ts",
|
|
10
|
+
"./convex.config": "./convex.config.ts"
|
|
11
|
+
},
|
|
8
12
|
"files": [
|
|
9
13
|
"convex.config.ts",
|
|
10
14
|
"README.md",
|
|
@@ -16,7 +20,7 @@
|
|
|
16
20
|
"prepack": "npm run sync:from-repo",
|
|
17
21
|
"test": "tsx --test tests/**/*.test.ts",
|
|
18
22
|
"smoke": "node scripts/smoke-test.mjs",
|
|
19
|
-
"version:patch": "
|
|
23
|
+
"version:patch": "node scripts/bump-version.mjs",
|
|
20
24
|
"publish:component": "npm run version:patch && npm publish"
|
|
21
25
|
},
|
|
22
26
|
"peerDependencies": {
|