@infuro/cms-core 1.0.28 → 1.0.30
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/admin.cjs +2571 -1418
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +10 -1
- package/dist/admin.d.ts +10 -1
- package/dist/admin.js +2549 -1386
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +735 -31
- package/dist/api.cjs.map +1 -1
- package/dist/api.d.cts +1 -1
- package/dist/api.d.ts +1 -1
- package/dist/api.js +739 -22
- package/dist/api.js.map +1 -1
- package/dist/cli.cjs +7 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/{index-CI6J9dxr.d.cts → index-BPQSXgXF.d.cts} +20 -1
- package/dist/{index-CMJZ5Fpr.d.ts → index-D9SdaDJ0.d.ts} +20 -1
- package/dist/index.cjs +2356 -1375
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -16
- package/dist/index.d.ts +107 -16
- package/dist/index.js +2347 -1364
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775700000000-JobSchedules.ts +70 -0
- package/package.json +11 -2
package/dist/api.cjs
CHANGED
|
@@ -213,6 +213,81 @@ var init_email_queue = __esm({
|
|
|
213
213
|
}
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
+
// src/plugins/pg-boss/pg-boss-service.ts
|
|
217
|
+
var pg_boss_service_exports = {};
|
|
218
|
+
__export(pg_boss_service_exports, {
|
|
219
|
+
JOB_RUNNER_QUEUE: () => JOB_RUNNER_QUEUE,
|
|
220
|
+
PgBossService: () => PgBossService
|
|
221
|
+
});
|
|
222
|
+
var import_pg_boss, JOB_RUNNER_QUEUE, PgBossService;
|
|
223
|
+
var init_pg_boss_service = __esm({
|
|
224
|
+
"src/plugins/pg-boss/pg-boss-service.ts"() {
|
|
225
|
+
"use strict";
|
|
226
|
+
import_pg_boss = __toESM(require("pg-boss"), 1);
|
|
227
|
+
JOB_RUNNER_QUEUE = "job-runner";
|
|
228
|
+
PgBossService = class {
|
|
229
|
+
boss;
|
|
230
|
+
started = false;
|
|
231
|
+
workRegistered = /* @__PURE__ */ new Set();
|
|
232
|
+
constructor(connectionString, schema) {
|
|
233
|
+
this.boss = new import_pg_boss.default({
|
|
234
|
+
connectionString,
|
|
235
|
+
...schema?.trim() ? { schema: schema.trim() } : {},
|
|
236
|
+
schedule: true
|
|
237
|
+
});
|
|
238
|
+
this.boss.on("error", (err) => {
|
|
239
|
+
console.error("[pg-boss]", err);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async start() {
|
|
243
|
+
if (this.started) return;
|
|
244
|
+
await this.boss.start();
|
|
245
|
+
await this.ensureQueue(JOB_RUNNER_QUEUE);
|
|
246
|
+
this.started = true;
|
|
247
|
+
}
|
|
248
|
+
async stop() {
|
|
249
|
+
if (!this.started) return;
|
|
250
|
+
await this.boss.stop({ graceful: true, timeout: 1e4 });
|
|
251
|
+
this.started = false;
|
|
252
|
+
this.workRegistered.clear();
|
|
253
|
+
}
|
|
254
|
+
get raw() {
|
|
255
|
+
return this.boss;
|
|
256
|
+
}
|
|
257
|
+
async ensureQueue(name) {
|
|
258
|
+
const existing = await this.boss.getQueue(name);
|
|
259
|
+
if (!existing) {
|
|
260
|
+
await this.boss.createQueue(name);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async send(queue, data) {
|
|
264
|
+
await this.ensureQueue(queue);
|
|
265
|
+
return this.boss.send(queue, data, { retryLimit: 2 });
|
|
266
|
+
}
|
|
267
|
+
async schedule(name, cron, data, options) {
|
|
268
|
+
await this.ensureQueue(name);
|
|
269
|
+
await this.boss.schedule(name, cron, data, options);
|
|
270
|
+
}
|
|
271
|
+
async unschedule(name) {
|
|
272
|
+
try {
|
|
273
|
+
await this.boss.unschedule(name);
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
async registerWork(queue, handler) {
|
|
278
|
+
if (this.workRegistered.has(queue)) return;
|
|
279
|
+
await this.ensureQueue(queue);
|
|
280
|
+
await this.boss.work(queue, async (jobs) => {
|
|
281
|
+
for (const job of jobs) {
|
|
282
|
+
await handler(job.data);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
this.workRegistered.add(queue);
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
216
291
|
// src/plugins/erp/erp-response-map.ts
|
|
217
292
|
function pickString(o, keys) {
|
|
218
293
|
for (const k of keys) {
|
|
@@ -508,6 +583,7 @@ __export(api_exports, {
|
|
|
508
583
|
createForgotPasswordHandler: () => createForgotPasswordHandler,
|
|
509
584
|
createFormBySlugHandler: () => createFormBySlugHandler,
|
|
510
585
|
createInviteAcceptHandler: () => createInviteAcceptHandler,
|
|
586
|
+
createJobScheduleHandlers: () => createJobScheduleHandlers,
|
|
511
587
|
createLlmAgentKnowledgeHandlers: () => createLlmAgentKnowledgeHandlers,
|
|
512
588
|
createMediaZipExtractHandler: () => createMediaZipExtractHandler,
|
|
513
589
|
createSetPasswordHandler: () => createSetPasswordHandler,
|
|
@@ -7355,6 +7431,145 @@ RssFeed = __decorateClass([
|
|
|
7355
7431
|
(0, import_typeorm47.Entity)("rss_feeds")
|
|
7356
7432
|
], RssFeed);
|
|
7357
7433
|
|
|
7434
|
+
// src/entities/job-schedule.entity.ts
|
|
7435
|
+
var import_typeorm49 = require("typeorm");
|
|
7436
|
+
|
|
7437
|
+
// src/entities/job-schedule-run.entity.ts
|
|
7438
|
+
var import_typeorm48 = require("typeorm");
|
|
7439
|
+
var JobScheduleRun = class {
|
|
7440
|
+
id;
|
|
7441
|
+
scheduleId;
|
|
7442
|
+
schedule;
|
|
7443
|
+
startedAt;
|
|
7444
|
+
finishedAt;
|
|
7445
|
+
status;
|
|
7446
|
+
error;
|
|
7447
|
+
result;
|
|
7448
|
+
triggeredBy;
|
|
7449
|
+
createdAt;
|
|
7450
|
+
};
|
|
7451
|
+
__decorateClass([
|
|
7452
|
+
(0, import_typeorm48.PrimaryGeneratedColumn)("uuid")
|
|
7453
|
+
], JobScheduleRun.prototype, "id", 2);
|
|
7454
|
+
__decorateClass([
|
|
7455
|
+
(0, import_typeorm48.Column)({ type: "uuid" })
|
|
7456
|
+
], JobScheduleRun.prototype, "scheduleId", 2);
|
|
7457
|
+
__decorateClass([
|
|
7458
|
+
(0, import_typeorm48.ManyToOne)(() => JobSchedule, (s) => s.runs, { onDelete: "CASCADE" }),
|
|
7459
|
+
(0, import_typeorm48.JoinColumn)({ name: "scheduleId" })
|
|
7460
|
+
], JobScheduleRun.prototype, "schedule", 2);
|
|
7461
|
+
__decorateClass([
|
|
7462
|
+
(0, import_typeorm48.Column)({ type: "timestamp" })
|
|
7463
|
+
], JobScheduleRun.prototype, "startedAt", 2);
|
|
7464
|
+
__decorateClass([
|
|
7465
|
+
(0, import_typeorm48.Column)({ type: "timestamp", nullable: true })
|
|
7466
|
+
], JobScheduleRun.prototype, "finishedAt", 2);
|
|
7467
|
+
__decorateClass([
|
|
7468
|
+
(0, import_typeorm48.Column)({ type: "varchar", length: 32 })
|
|
7469
|
+
], JobScheduleRun.prototype, "status", 2);
|
|
7470
|
+
__decorateClass([
|
|
7471
|
+
(0, import_typeorm48.Column)({ type: "text", nullable: true })
|
|
7472
|
+
], JobScheduleRun.prototype, "error", 2);
|
|
7473
|
+
__decorateClass([
|
|
7474
|
+
(0, import_typeorm48.Column)({ type: "jsonb", nullable: true })
|
|
7475
|
+
], JobScheduleRun.prototype, "result", 2);
|
|
7476
|
+
__decorateClass([
|
|
7477
|
+
(0, import_typeorm48.Column)({ type: "varchar", length: 16, default: "schedule" })
|
|
7478
|
+
], JobScheduleRun.prototype, "triggeredBy", 2);
|
|
7479
|
+
__decorateClass([
|
|
7480
|
+
(0, import_typeorm48.CreateDateColumn)()
|
|
7481
|
+
], JobScheduleRun.prototype, "createdAt", 2);
|
|
7482
|
+
JobScheduleRun = __decorateClass([
|
|
7483
|
+
(0, import_typeorm48.Entity)("job_schedule_runs")
|
|
7484
|
+
], JobScheduleRun);
|
|
7485
|
+
|
|
7486
|
+
// src/entities/job-schedule.entity.ts
|
|
7487
|
+
var JobSchedule = class {
|
|
7488
|
+
id;
|
|
7489
|
+
name;
|
|
7490
|
+
jobType;
|
|
7491
|
+
enabled;
|
|
7492
|
+
scheduleMode;
|
|
7493
|
+
intervalMinutes;
|
|
7494
|
+
runAtTime;
|
|
7495
|
+
runOnDays;
|
|
7496
|
+
timezone;
|
|
7497
|
+
cronExpression;
|
|
7498
|
+
payload;
|
|
7499
|
+
authorId;
|
|
7500
|
+
pgBossScheduleName;
|
|
7501
|
+
lastRunAt;
|
|
7502
|
+
lastRunStatus;
|
|
7503
|
+
lastRunError;
|
|
7504
|
+
nextRunAt;
|
|
7505
|
+
runs;
|
|
7506
|
+
createdAt;
|
|
7507
|
+
updatedAt;
|
|
7508
|
+
};
|
|
7509
|
+
__decorateClass([
|
|
7510
|
+
(0, import_typeorm49.PrimaryGeneratedColumn)("uuid")
|
|
7511
|
+
], JobSchedule.prototype, "id", 2);
|
|
7512
|
+
__decorateClass([
|
|
7513
|
+
(0, import_typeorm49.Column)({ type: "text" })
|
|
7514
|
+
], JobSchedule.prototype, "name", 2);
|
|
7515
|
+
__decorateClass([
|
|
7516
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 64, default: "blog_generate" })
|
|
7517
|
+
], JobSchedule.prototype, "jobType", 2);
|
|
7518
|
+
__decorateClass([
|
|
7519
|
+
(0, import_typeorm49.Column)({ type: "boolean", default: false })
|
|
7520
|
+
], JobSchedule.prototype, "enabled", 2);
|
|
7521
|
+
__decorateClass([
|
|
7522
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 32, default: "daily" })
|
|
7523
|
+
], JobSchedule.prototype, "scheduleMode", 2);
|
|
7524
|
+
__decorateClass([
|
|
7525
|
+
(0, import_typeorm49.Column)({ type: "int", nullable: true })
|
|
7526
|
+
], JobSchedule.prototype, "intervalMinutes", 2);
|
|
7527
|
+
__decorateClass([
|
|
7528
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 8, nullable: true })
|
|
7529
|
+
], JobSchedule.prototype, "runAtTime", 2);
|
|
7530
|
+
__decorateClass([
|
|
7531
|
+
(0, import_typeorm49.Column)({ type: "jsonb", nullable: true })
|
|
7532
|
+
], JobSchedule.prototype, "runOnDays", 2);
|
|
7533
|
+
__decorateClass([
|
|
7534
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 64, default: "UTC" })
|
|
7535
|
+
], JobSchedule.prototype, "timezone", 2);
|
|
7536
|
+
__decorateClass([
|
|
7537
|
+
(0, import_typeorm49.Column)({ type: "text", nullable: true })
|
|
7538
|
+
], JobSchedule.prototype, "cronExpression", 2);
|
|
7539
|
+
__decorateClass([
|
|
7540
|
+
(0, import_typeorm49.Column)({ type: "jsonb", default: {} })
|
|
7541
|
+
], JobSchedule.prototype, "payload", 2);
|
|
7542
|
+
__decorateClass([
|
|
7543
|
+
(0, import_typeorm49.Column)({ type: "int", nullable: true })
|
|
7544
|
+
], JobSchedule.prototype, "authorId", 2);
|
|
7545
|
+
__decorateClass([
|
|
7546
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 128, unique: true })
|
|
7547
|
+
], JobSchedule.prototype, "pgBossScheduleName", 2);
|
|
7548
|
+
__decorateClass([
|
|
7549
|
+
(0, import_typeorm49.Column)({ type: "timestamp", nullable: true })
|
|
7550
|
+
], JobSchedule.prototype, "lastRunAt", 2);
|
|
7551
|
+
__decorateClass([
|
|
7552
|
+
(0, import_typeorm49.Column)({ type: "varchar", length: 32, nullable: true })
|
|
7553
|
+
], JobSchedule.prototype, "lastRunStatus", 2);
|
|
7554
|
+
__decorateClass([
|
|
7555
|
+
(0, import_typeorm49.Column)({ type: "text", nullable: true })
|
|
7556
|
+
], JobSchedule.prototype, "lastRunError", 2);
|
|
7557
|
+
__decorateClass([
|
|
7558
|
+
(0, import_typeorm49.Column)({ type: "timestamp", nullable: true })
|
|
7559
|
+
], JobSchedule.prototype, "nextRunAt", 2);
|
|
7560
|
+
__decorateClass([
|
|
7561
|
+
(0, import_typeorm49.OneToMany)(() => JobScheduleRun, (r) => r.schedule)
|
|
7562
|
+
], JobSchedule.prototype, "runs", 2);
|
|
7563
|
+
__decorateClass([
|
|
7564
|
+
(0, import_typeorm49.CreateDateColumn)()
|
|
7565
|
+
], JobSchedule.prototype, "createdAt", 2);
|
|
7566
|
+
__decorateClass([
|
|
7567
|
+
(0, import_typeorm49.UpdateDateColumn)()
|
|
7568
|
+
], JobSchedule.prototype, "updatedAt", 2);
|
|
7569
|
+
JobSchedule = __decorateClass([
|
|
7570
|
+
(0, import_typeorm49.Entity)("job_schedules")
|
|
7571
|
+
], JobSchedule);
|
|
7572
|
+
|
|
7358
7573
|
// src/entities/index.ts
|
|
7359
7574
|
var CMS_ENTITY_MAP = {
|
|
7360
7575
|
users: User,
|
|
@@ -7398,7 +7613,9 @@ var CMS_ENTITY_MAP = {
|
|
|
7398
7613
|
llm_agents: LlmAgent,
|
|
7399
7614
|
llm_agent_knowledge_documents: LlmAgentKnowledgeDocument,
|
|
7400
7615
|
rss_feeds: RssFeed,
|
|
7401
|
-
rss_articles: RssArticle
|
|
7616
|
+
rss_articles: RssArticle,
|
|
7617
|
+
job_schedules: JobSchedule,
|
|
7618
|
+
job_schedule_runs: JobScheduleRun
|
|
7402
7619
|
};
|
|
7403
7620
|
|
|
7404
7621
|
// src/plugins/blog-generator/blog-generator-service.ts
|
|
@@ -7746,6 +7963,13 @@ var import_node_path = __toESM(require("path"), 1);
|
|
|
7746
7963
|
|
|
7747
7964
|
// src/plugins/social-media/linkedin-client.ts
|
|
7748
7965
|
var RESTLI = "2.0.0";
|
|
7966
|
+
function linkedInHeaders(accessToken, json = false) {
|
|
7967
|
+
return {
|
|
7968
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7969
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7970
|
+
...json ? { "Content-Type": "application/json" } : {}
|
|
7971
|
+
};
|
|
7972
|
+
}
|
|
7749
7973
|
async function linkedInGetUserinfo(accessToken) {
|
|
7750
7974
|
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7751
7975
|
headers: { Authorization: `Bearer ${accessToken}` }
|
|
@@ -7760,20 +7984,116 @@ async function linkedInGetUserinfo(accessToken) {
|
|
|
7760
7984
|
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7761
7985
|
}
|
|
7762
7986
|
}
|
|
7987
|
+
async function linkedInFetchOrganizationAcls(accessToken) {
|
|
7988
|
+
const r = await fetch("https://api.linkedin.com/v2/organizationAcls?q=roleAssignee", {
|
|
7989
|
+
headers: linkedInHeaders(accessToken)
|
|
7990
|
+
});
|
|
7991
|
+
const text = await r.text();
|
|
7992
|
+
if (!r.ok) {
|
|
7993
|
+
throw new Error(`LinkedIn organizationAcls failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7994
|
+
}
|
|
7995
|
+
let data;
|
|
7996
|
+
try {
|
|
7997
|
+
data = JSON.parse(text);
|
|
7998
|
+
} catch {
|
|
7999
|
+
throw new Error("LinkedIn organizationAcls: invalid JSON");
|
|
8000
|
+
}
|
|
8001
|
+
return Array.isArray(data.elements) ? data.elements : [];
|
|
8002
|
+
}
|
|
8003
|
+
function organizationDisplayName(org) {
|
|
8004
|
+
if (org.localizedName?.trim()) return org.localizedName.trim();
|
|
8005
|
+
const localized = org.name?.localized;
|
|
8006
|
+
if (localized) {
|
|
8007
|
+
for (const v of Object.values(localized)) {
|
|
8008
|
+
if (v?.trim()) return v.trim();
|
|
8009
|
+
}
|
|
8010
|
+
}
|
|
8011
|
+
if (org.vanityName?.trim()) return org.vanityName.trim();
|
|
8012
|
+
if (org.id != null) return `Organization ${org.id}`;
|
|
8013
|
+
return "Organization";
|
|
8014
|
+
}
|
|
8015
|
+
function organizationWebsite(org) {
|
|
8016
|
+
if (org.localizedWebsite?.trim()) return org.localizedWebsite.trim();
|
|
8017
|
+
const localized = org.website?.localized;
|
|
8018
|
+
if (localized) {
|
|
8019
|
+
for (const v of Object.values(localized)) {
|
|
8020
|
+
if (v?.trim()) return v.trim();
|
|
8021
|
+
}
|
|
8022
|
+
}
|
|
8023
|
+
return void 0;
|
|
8024
|
+
}
|
|
8025
|
+
function linkedInOrganizationUrn(orgIdOrUrn) {
|
|
8026
|
+
const s = String(orgIdOrUrn ?? "").trim();
|
|
8027
|
+
if (s.startsWith("urn:li:organization:")) return s;
|
|
8028
|
+
return `urn:li:organization:${s}`;
|
|
8029
|
+
}
|
|
8030
|
+
function linkedInOrganizationIdFromUrn(organizationUrn) {
|
|
8031
|
+
const m = String(organizationUrn).trim().match(/urn:li:organization:(\d+)/);
|
|
8032
|
+
if (!m?.[1]) throw new Error(`Invalid LinkedIn organization URN: ${organizationUrn}`);
|
|
8033
|
+
return m[1];
|
|
8034
|
+
}
|
|
8035
|
+
async function linkedInGetOrganization(accessToken, orgIdOrUrn) {
|
|
8036
|
+
const orgId = String(orgIdOrUrn).startsWith("urn:") ? linkedInOrganizationIdFromUrn(String(orgIdOrUrn)) : String(orgIdOrUrn).trim();
|
|
8037
|
+
const r = await fetch(`https://api.linkedin.com/v2/organizations/${encodeURIComponent(orgId)}`, {
|
|
8038
|
+
headers: linkedInHeaders(accessToken)
|
|
8039
|
+
});
|
|
8040
|
+
const text = await r.text();
|
|
8041
|
+
if (!r.ok) {
|
|
8042
|
+
throw new Error(`LinkedIn organization ${orgId} failed (${r.status}): ${text.slice(0, 800)}`);
|
|
8043
|
+
}
|
|
8044
|
+
try {
|
|
8045
|
+
return JSON.parse(text);
|
|
8046
|
+
} catch {
|
|
8047
|
+
throw new Error(`LinkedIn organization ${orgId}: invalid JSON`);
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
async function linkedInListManagedOrganizations(accessToken) {
|
|
8051
|
+
const acls = await linkedInFetchOrganizationAcls(accessToken);
|
|
8052
|
+
const approved = acls.filter((a) => a.state === "APPROVED" && a.organization?.trim());
|
|
8053
|
+
const byUrn = /* @__PURE__ */ new Map();
|
|
8054
|
+
for (const acl of approved) {
|
|
8055
|
+
const urn = linkedInOrganizationUrn(acl.organization);
|
|
8056
|
+
if (!byUrn.has(urn)) byUrn.set(urn, acl);
|
|
8057
|
+
}
|
|
8058
|
+
const out = [];
|
|
8059
|
+
for (const [urn, acl] of byUrn) {
|
|
8060
|
+
try {
|
|
8061
|
+
const org = await linkedInGetOrganization(accessToken, urn);
|
|
8062
|
+
out.push({
|
|
8063
|
+
urn,
|
|
8064
|
+
id: org.id ?? Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8065
|
+
name: organizationDisplayName(org),
|
|
8066
|
+
vanityName: org.vanityName,
|
|
8067
|
+
website: organizationWebsite(org),
|
|
8068
|
+
role: acl.role
|
|
8069
|
+
});
|
|
8070
|
+
} catch {
|
|
8071
|
+
out.push({
|
|
8072
|
+
urn,
|
|
8073
|
+
id: Number(linkedInOrganizationIdFromUrn(urn)),
|
|
8074
|
+
name: linkedInOrganizationIdFromUrn(urn),
|
|
8075
|
+
role: acl.role
|
|
8076
|
+
});
|
|
8077
|
+
}
|
|
8078
|
+
}
|
|
8079
|
+
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
8080
|
+
return out;
|
|
8081
|
+
}
|
|
7763
8082
|
function personUrn(personSub) {
|
|
7764
8083
|
const s = String(personSub || "").trim();
|
|
7765
8084
|
if (s.startsWith("urn:li:person:")) return s;
|
|
7766
8085
|
return `urn:li:person:${s}`;
|
|
7767
8086
|
}
|
|
7768
|
-
|
|
7769
|
-
const
|
|
8087
|
+
function authorUrnFromParams(authorUrn, personSub) {
|
|
8088
|
+
const direct = String(authorUrn ?? "").trim();
|
|
8089
|
+
if (direct.startsWith("urn:li:")) return direct;
|
|
8090
|
+
return personUrn(String(personSub ?? "").trim());
|
|
8091
|
+
}
|
|
8092
|
+
async function linkedInRegisterImageUpload(accessToken, author) {
|
|
8093
|
+
const owner = authorUrnFromParams(author);
|
|
7770
8094
|
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7771
8095
|
method: "POST",
|
|
7772
|
-
headers:
|
|
7773
|
-
Authorization: `Bearer ${accessToken}`,
|
|
7774
|
-
"X-Restli-Protocol-Version": RESTLI,
|
|
7775
|
-
"Content-Type": "application/json"
|
|
7776
|
-
},
|
|
8096
|
+
headers: linkedInHeaders(accessToken, true),
|
|
7777
8097
|
body: JSON.stringify({
|
|
7778
8098
|
registerUploadRequest: {
|
|
7779
8099
|
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
@@ -7821,7 +8141,7 @@ async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
|
7821
8141
|
}
|
|
7822
8142
|
}
|
|
7823
8143
|
async function linkedInCreateImageShare(params) {
|
|
7824
|
-
const author =
|
|
8144
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
7825
8145
|
const commentary = params.commentary.slice(0, 2900);
|
|
7826
8146
|
const title = params.title.slice(0, 200);
|
|
7827
8147
|
const description = (params.description ?? title).slice(0, 200);
|
|
@@ -7867,7 +8187,7 @@ async function linkedInCreateImageShare(params) {
|
|
|
7867
8187
|
return { status: r.status, id: data.id };
|
|
7868
8188
|
}
|
|
7869
8189
|
async function linkedInCreateTextShare(params) {
|
|
7870
|
-
const author =
|
|
8190
|
+
const author = authorUrnFromParams(params.authorUrn);
|
|
7871
8191
|
const commentary = params.commentary.slice(0, 2900);
|
|
7872
8192
|
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7873
8193
|
method: "POST",
|
|
@@ -8201,18 +8521,44 @@ function createSocialMediaHandlers(config) {
|
|
|
8201
8521
|
const enabled = map.enabled !== "false";
|
|
8202
8522
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8203
8523
|
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
8524
|
+
const orgUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8204
8525
|
return json({
|
|
8205
8526
|
ok: true,
|
|
8206
|
-
linkedInReady: enabled && Boolean(token) && Boolean(
|
|
8527
|
+
linkedInReady: enabled && Boolean(token) && Boolean(orgUrn),
|
|
8207
8528
|
enabled,
|
|
8208
8529
|
hasToken: Boolean(token),
|
|
8209
|
-
hasPersonSub: Boolean(sub)
|
|
8530
|
+
hasPersonSub: Boolean(sub),
|
|
8531
|
+
hasOrganization: Boolean(orgUrn),
|
|
8532
|
+
organizationName: map.linkedin_organization_name?.trim() || null
|
|
8210
8533
|
});
|
|
8211
8534
|
} catch (e) {
|
|
8212
8535
|
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8213
8536
|
return json({ error: msg }, { status: 500 });
|
|
8214
8537
|
}
|
|
8215
8538
|
},
|
|
8539
|
+
async fetchLinkedInOrganizations(req) {
|
|
8540
|
+
const a = await requireAuth(req);
|
|
8541
|
+
if (a) return a;
|
|
8542
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
8543
|
+
if (pe) return pe;
|
|
8544
|
+
let body = {};
|
|
8545
|
+
try {
|
|
8546
|
+
body = await req.json();
|
|
8547
|
+
} catch {
|
|
8548
|
+
}
|
|
8549
|
+
try {
|
|
8550
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8551
|
+
const token = String(body?.accessToken ?? map.linkedin_access_token ?? "").trim();
|
|
8552
|
+
if (!token) {
|
|
8553
|
+
return json({ error: "LinkedIn access token is required." }, { status: 400 });
|
|
8554
|
+
}
|
|
8555
|
+
const organizations = await linkedInListManagedOrganizations(token);
|
|
8556
|
+
return json({ ok: true, organizations });
|
|
8557
|
+
} catch (e) {
|
|
8558
|
+
const msg = e instanceof Error ? e.message : "LinkedIn organizations request failed";
|
|
8559
|
+
return json({ error: msg }, { status: 502 });
|
|
8560
|
+
}
|
|
8561
|
+
},
|
|
8216
8562
|
async syncLinkedInProfile(req) {
|
|
8217
8563
|
const a = await requireAuth(req);
|
|
8218
8564
|
if (a) return a;
|
|
@@ -8261,10 +8607,12 @@ function createSocialMediaHandlers(config) {
|
|
|
8261
8607
|
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8262
8608
|
}
|
|
8263
8609
|
const token = (map.linkedin_access_token ?? "").trim();
|
|
8264
|
-
const
|
|
8265
|
-
if (!token || !
|
|
8610
|
+
const authorUrn = (map.linkedin_organization_urn ?? "").trim();
|
|
8611
|
+
if (!token || !authorUrn) {
|
|
8266
8612
|
return json(
|
|
8267
|
-
{
|
|
8613
|
+
{
|
|
8614
|
+
error: "Configure LinkedIn access token and target organization (Plugins \u2192 Social media \u2192 LinkedIn)."
|
|
8615
|
+
},
|
|
8268
8616
|
{ status: 400 }
|
|
8269
8617
|
);
|
|
8270
8618
|
}
|
|
@@ -8305,7 +8653,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8305
8653
|
contentType: imagePayload.contentType
|
|
8306
8654
|
});
|
|
8307
8655
|
try {
|
|
8308
|
-
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token,
|
|
8656
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, authorUrn);
|
|
8309
8657
|
logLinkedInPublish(publishLog, "image_registered", {
|
|
8310
8658
|
blogId,
|
|
8311
8659
|
asset: truncateForLog(asset, 120),
|
|
@@ -8321,7 +8669,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8321
8669
|
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
8322
8670
|
const out2 = await linkedInCreateImageShare({
|
|
8323
8671
|
accessToken: token,
|
|
8324
|
-
|
|
8672
|
+
authorUrn,
|
|
8325
8673
|
asset,
|
|
8326
8674
|
commentary,
|
|
8327
8675
|
title,
|
|
@@ -8359,7 +8707,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8359
8707
|
});
|
|
8360
8708
|
const out2 = await linkedInCreateTextShare({
|
|
8361
8709
|
accessToken: token,
|
|
8362
|
-
|
|
8710
|
+
authorUrn,
|
|
8363
8711
|
commentary
|
|
8364
8712
|
});
|
|
8365
8713
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8398,7 +8746,7 @@ ${excerpt}`).trim().slice(0, 2900);
|
|
|
8398
8746
|
});
|
|
8399
8747
|
const out = await linkedInCreateTextShare({
|
|
8400
8748
|
accessToken: token,
|
|
8401
|
-
|
|
8749
|
+
authorUrn,
|
|
8402
8750
|
commentary
|
|
8403
8751
|
});
|
|
8404
8752
|
logLinkedInPublish(publishLog, "success", {
|
|
@@ -8601,6 +8949,323 @@ ${excerpt}`).trim().slice(0, 2200);
|
|
|
8601
8949
|
};
|
|
8602
8950
|
}
|
|
8603
8951
|
|
|
8952
|
+
// src/api/job-schedule-handlers.ts
|
|
8953
|
+
var import_crypto = require("crypto");
|
|
8954
|
+
|
|
8955
|
+
// src/plugins/jobs/schedule-cron.ts
|
|
8956
|
+
function pgBossScheduleNameForId(scheduleId) {
|
|
8957
|
+
return `schedule:${scheduleId}`;
|
|
8958
|
+
}
|
|
8959
|
+
function buildCronFromSchedule(schedule) {
|
|
8960
|
+
if (schedule.scheduleMode === "cron" && schedule.cronExpression?.trim()) {
|
|
8961
|
+
return schedule.cronExpression.trim();
|
|
8962
|
+
}
|
|
8963
|
+
const time = parseRunAtTime(schedule.runAtTime);
|
|
8964
|
+
const minute = time?.minute ?? 0;
|
|
8965
|
+
const hour = time?.hour ?? 9;
|
|
8966
|
+
if (schedule.scheduleMode === "interval") {
|
|
8967
|
+
const mins = schedule.intervalMinutes ?? 60;
|
|
8968
|
+
if (mins >= 60 && mins % 60 === 0) {
|
|
8969
|
+
const h = Math.max(1, Math.min(23, Math.floor(mins / 60)));
|
|
8970
|
+
if (h === 1) return `${minute} * * * *`;
|
|
8971
|
+
return `${minute} */${h} * * *`;
|
|
8972
|
+
}
|
|
8973
|
+
if (mins <= 59) {
|
|
8974
|
+
return `*/${Math.max(1, mins)} * * * *`;
|
|
8975
|
+
}
|
|
8976
|
+
return `${minute} * * * *`;
|
|
8977
|
+
}
|
|
8978
|
+
if (schedule.scheduleMode === "weekly") {
|
|
8979
|
+
const days = Array.isArray(schedule.runOnDays) && schedule.runOnDays.length > 0 ? schedule.runOnDays.filter((d) => d >= 0 && d <= 6).join(",") : "1";
|
|
8980
|
+
return `${minute} ${hour} * * ${days}`;
|
|
8981
|
+
}
|
|
8982
|
+
return `${minute} ${hour} * * *`;
|
|
8983
|
+
}
|
|
8984
|
+
function parseRunAtTime(runAtTime) {
|
|
8985
|
+
if (!runAtTime?.trim()) return null;
|
|
8986
|
+
const m = runAtTime.trim().match(/^(\d{1,2}):(\d{2})$/);
|
|
8987
|
+
if (!m) return null;
|
|
8988
|
+
const hour = parseInt(m[1], 10);
|
|
8989
|
+
const minute = parseInt(m[2], 10);
|
|
8990
|
+
if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null;
|
|
8991
|
+
return { hour, minute };
|
|
8992
|
+
}
|
|
8993
|
+
function validateScheduleInput(schedule) {
|
|
8994
|
+
if (schedule.scheduleMode === "cron") {
|
|
8995
|
+
if (!schedule.cronExpression?.trim()) return "cronExpression is required for cron mode";
|
|
8996
|
+
return null;
|
|
8997
|
+
}
|
|
8998
|
+
if (schedule.scheduleMode === "interval") {
|
|
8999
|
+
const mins = schedule.intervalMinutes;
|
|
9000
|
+
if (mins == null || !Number.isFinite(mins) || mins < 1 || mins > 10080) {
|
|
9001
|
+
return "intervalMinutes must be between 1 and 10080";
|
|
9002
|
+
}
|
|
9003
|
+
return null;
|
|
9004
|
+
}
|
|
9005
|
+
if (schedule.runAtTime?.trim()) {
|
|
9006
|
+
if (!parseRunAtTime(schedule.runAtTime)) return "runAtTime must be HH:mm (24h)";
|
|
9007
|
+
}
|
|
9008
|
+
return null;
|
|
9009
|
+
}
|
|
9010
|
+
|
|
9011
|
+
// src/plugins/jobs/schedule-sync.ts
|
|
9012
|
+
async function syncJobScheduleToPgBoss(cms, schedule, onRegisterQueue) {
|
|
9013
|
+
const boss = cms.getPlugin("pg_boss");
|
|
9014
|
+
if (!boss) return;
|
|
9015
|
+
const name = schedule.pgBossScheduleName;
|
|
9016
|
+
await boss.unschedule(name);
|
|
9017
|
+
if (!schedule.enabled) return;
|
|
9018
|
+
const cron = buildCronFromSchedule(schedule);
|
|
9019
|
+
const data = {
|
|
9020
|
+
scheduleId: schedule.id,
|
|
9021
|
+
triggeredBy: "schedule"
|
|
9022
|
+
};
|
|
9023
|
+
await boss.schedule(name, cron, data, { tz: schedule.timezone || "UTC" });
|
|
9024
|
+
if (onRegisterQueue) {
|
|
9025
|
+
await onRegisterQueue(name);
|
|
9026
|
+
}
|
|
9027
|
+
}
|
|
9028
|
+
async function queueJobScheduleNow(cms, scheduleId) {
|
|
9029
|
+
const boss = cms.getPlugin("pg_boss");
|
|
9030
|
+
if (!boss) {
|
|
9031
|
+
throw new Error("pg-boss is not configured (DATABASE_URL required)");
|
|
9032
|
+
}
|
|
9033
|
+
const { JOB_RUNNER_QUEUE: JOB_RUNNER_QUEUE2 } = await Promise.resolve().then(() => (init_pg_boss_service(), pg_boss_service_exports));
|
|
9034
|
+
await boss.send(JOB_RUNNER_QUEUE2, {
|
|
9035
|
+
scheduleId,
|
|
9036
|
+
triggeredBy: "manual"
|
|
9037
|
+
});
|
|
9038
|
+
}
|
|
9039
|
+
|
|
9040
|
+
// src/plugins/jobs/job-runner.ts
|
|
9041
|
+
init_pg_boss_service();
|
|
9042
|
+
|
|
9043
|
+
// src/plugins/jobs/blog-generate-job.ts
|
|
9044
|
+
var import_typeorm50 = require("typeorm");
|
|
9045
|
+
|
|
9046
|
+
// src/plugins/jobs/schedule-next-run.ts
|
|
9047
|
+
var import_cron_parser = require("cron-parser");
|
|
9048
|
+
function computeNextRunAt(schedule, from = /* @__PURE__ */ new Date()) {
|
|
9049
|
+
if (!schedule.enabled) return null;
|
|
9050
|
+
try {
|
|
9051
|
+
const cron = buildCronFromSchedule(schedule);
|
|
9052
|
+
const tz = schedule.timezone?.trim() || "UTC";
|
|
9053
|
+
const interval = (0, import_cron_parser.parseExpression)(cron, {
|
|
9054
|
+
currentDate: from,
|
|
9055
|
+
tz
|
|
9056
|
+
});
|
|
9057
|
+
return interval.next().toDate();
|
|
9058
|
+
} catch (err) {
|
|
9059
|
+
console.warn("[job-schedules] could not compute next run:", err);
|
|
9060
|
+
return null;
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
// src/plugins/jobs/job-runner.ts
|
|
9065
|
+
var executeJobRef = null;
|
|
9066
|
+
async function ensureScheduleQueueWorker(cms, queueName) {
|
|
9067
|
+
const boss = cms.getPlugin("pg_boss");
|
|
9068
|
+
if (!boss || !executeJobRef) return;
|
|
9069
|
+
await boss.registerWork(queueName, executeJobRef);
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
// src/api/job-schedule-handlers.ts
|
|
9073
|
+
function serializeSchedule(s) {
|
|
9074
|
+
return {
|
|
9075
|
+
id: s.id,
|
|
9076
|
+
name: s.name,
|
|
9077
|
+
jobType: s.jobType,
|
|
9078
|
+
enabled: s.enabled,
|
|
9079
|
+
scheduleMode: s.scheduleMode,
|
|
9080
|
+
intervalMinutes: s.intervalMinutes,
|
|
9081
|
+
runAtTime: s.runAtTime,
|
|
9082
|
+
runOnDays: s.runOnDays,
|
|
9083
|
+
timezone: s.timezone,
|
|
9084
|
+
cronExpression: s.cronExpression,
|
|
9085
|
+
cronResolved: buildCronFromSchedule(s),
|
|
9086
|
+
payload: s.payload ?? {},
|
|
9087
|
+
authorId: s.authorId,
|
|
9088
|
+
pgBossScheduleName: s.pgBossScheduleName,
|
|
9089
|
+
lastRunAt: s.lastRunAt?.toISOString() ?? null,
|
|
9090
|
+
lastRunStatus: s.lastRunStatus,
|
|
9091
|
+
lastRunError: s.lastRunError,
|
|
9092
|
+
nextRunAt: (computeNextRunAt(s) ?? s.nextRunAt)?.toISOString() ?? null,
|
|
9093
|
+
createdAt: s.createdAt.toISOString(),
|
|
9094
|
+
updatedAt: s.updatedAt.toISOString()
|
|
9095
|
+
};
|
|
9096
|
+
}
|
|
9097
|
+
function serializeRun(r) {
|
|
9098
|
+
return {
|
|
9099
|
+
id: r.id,
|
|
9100
|
+
scheduleId: r.scheduleId,
|
|
9101
|
+
startedAt: r.startedAt.toISOString(),
|
|
9102
|
+
finishedAt: r.finishedAt?.toISOString() ?? null,
|
|
9103
|
+
status: r.status,
|
|
9104
|
+
error: r.error,
|
|
9105
|
+
result: r.result,
|
|
9106
|
+
triggeredBy: r.triggeredBy,
|
|
9107
|
+
createdAt: r.createdAt.toISOString()
|
|
9108
|
+
};
|
|
9109
|
+
}
|
|
9110
|
+
function parseScheduleBody(body, existing) {
|
|
9111
|
+
const scheduleMode = typeof body.scheduleMode === "string" ? body.scheduleMode : existing?.scheduleMode ?? "daily";
|
|
9112
|
+
const patch = {
|
|
9113
|
+
name: typeof body.name === "string" ? body.name.trim().slice(0, 500) : existing?.name,
|
|
9114
|
+
jobType: body.jobType === "blog_generate" ? "blog_generate" : existing?.jobType ?? "blog_generate",
|
|
9115
|
+
enabled: typeof body.enabled === "boolean" ? body.enabled : existing?.enabled ?? false,
|
|
9116
|
+
scheduleMode,
|
|
9117
|
+
intervalMinutes: typeof body.intervalMinutes === "number" ? body.intervalMinutes : body.intervalMinutes != null ? Number(body.intervalMinutes) : existing?.intervalMinutes ?? null,
|
|
9118
|
+
runAtTime: typeof body.runAtTime === "string" ? body.runAtTime.trim() || null : body.runAtTime === null ? null : existing?.runAtTime ?? null,
|
|
9119
|
+
runOnDays: Array.isArray(body.runOnDays) ? body.runOnDays.map((d) => Number(d)).filter((d) => Number.isFinite(d)) : existing?.runOnDays ?? null,
|
|
9120
|
+
timezone: typeof body.timezone === "string" && body.timezone.trim() ? body.timezone.trim().slice(0, 64) : existing?.timezone ?? "UTC",
|
|
9121
|
+
cronExpression: typeof body.cronExpression === "string" ? body.cronExpression.trim() || null : existing?.cronExpression ?? null,
|
|
9122
|
+
payload: body.payload != null && typeof body.payload === "object" && !Array.isArray(body.payload) ? body.payload : existing?.payload ?? {},
|
|
9123
|
+
authorId: typeof body.authorId === "number" ? body.authorId : body.authorId != null ? Number(body.authorId) : existing?.authorId ?? null
|
|
9124
|
+
};
|
|
9125
|
+
return patch;
|
|
9126
|
+
}
|
|
9127
|
+
function createJobScheduleHandlers(apiConfig) {
|
|
9128
|
+
const { dataSource, entityMap, json, requireAuth, requireEntityPermission, getCms } = apiConfig;
|
|
9129
|
+
const ScheduleEntity = entityMap.job_schedules ?? JobSchedule;
|
|
9130
|
+
const RunEntity = entityMap.job_schedule_runs ?? JobScheduleRun;
|
|
9131
|
+
async function perm(req, action) {
|
|
9132
|
+
if (!requireEntityPermission) return null;
|
|
9133
|
+
return requireEntityPermission(req, "blogs", action);
|
|
9134
|
+
}
|
|
9135
|
+
async function syncAfterSave(cms, schedule) {
|
|
9136
|
+
await syncJobScheduleToPgBoss(cms, schedule, async (queueName) => {
|
|
9137
|
+
await ensureScheduleQueueWorker(cms, queueName);
|
|
9138
|
+
});
|
|
9139
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9140
|
+
schedule.nextRunAt = computeNextRunAt(schedule);
|
|
9141
|
+
await repo.save(schedule);
|
|
9142
|
+
}
|
|
9143
|
+
return {
|
|
9144
|
+
async list(req) {
|
|
9145
|
+
const a = await requireAuth(req);
|
|
9146
|
+
if (a) return a;
|
|
9147
|
+
const pe = await perm(req, "read");
|
|
9148
|
+
if (pe) return pe;
|
|
9149
|
+
const list = await dataSource.getRepository(ScheduleEntity).find({
|
|
9150
|
+
order: { updatedAt: "DESC" }
|
|
9151
|
+
});
|
|
9152
|
+
return json({ schedules: list.map(serializeSchedule) });
|
|
9153
|
+
},
|
|
9154
|
+
async getById(req, id) {
|
|
9155
|
+
const a = await requireAuth(req);
|
|
9156
|
+
if (a) return a;
|
|
9157
|
+
const pe = await perm(req, "read");
|
|
9158
|
+
if (pe) return pe;
|
|
9159
|
+
const row = await dataSource.getRepository(ScheduleEntity).findOne({ where: { id } });
|
|
9160
|
+
if (!row) return json({ error: "Schedule not found" }, { status: 404 });
|
|
9161
|
+
const runs = await dataSource.getRepository(RunEntity).find({
|
|
9162
|
+
where: { scheduleId: id },
|
|
9163
|
+
order: { startedAt: "DESC" },
|
|
9164
|
+
take: 20
|
|
9165
|
+
});
|
|
9166
|
+
return json({ schedule: serializeSchedule(row), runs: runs.map(serializeRun) });
|
|
9167
|
+
},
|
|
9168
|
+
async create(req) {
|
|
9169
|
+
const a = await requireAuth(req);
|
|
9170
|
+
if (a) return a;
|
|
9171
|
+
const pe = await perm(req, "create");
|
|
9172
|
+
if (pe) return pe;
|
|
9173
|
+
if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
|
|
9174
|
+
let body;
|
|
9175
|
+
try {
|
|
9176
|
+
body = await req.json();
|
|
9177
|
+
} catch {
|
|
9178
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9179
|
+
}
|
|
9180
|
+
const patch = parseScheduleBody(body);
|
|
9181
|
+
if (!patch.name) return json({ error: "name is required" }, { status: 400 });
|
|
9182
|
+
const draft = Object.assign(new JobSchedule(), patch);
|
|
9183
|
+
const validationError = validateScheduleInput(draft);
|
|
9184
|
+
if (validationError) return json({ error: validationError }, { status: 400 });
|
|
9185
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9186
|
+
let row = repo.create({
|
|
9187
|
+
...patch,
|
|
9188
|
+
pgBossScheduleName: `pending-${(0, import_crypto.randomUUID)()}`
|
|
9189
|
+
});
|
|
9190
|
+
row = await repo.save(row);
|
|
9191
|
+
row.pgBossScheduleName = pgBossScheduleNameForId(row.id);
|
|
9192
|
+
row = await repo.save(row);
|
|
9193
|
+
const cms = await getCms();
|
|
9194
|
+
await syncAfterSave(cms, row);
|
|
9195
|
+
return json({ schedule: serializeSchedule(row) }, { status: 201 });
|
|
9196
|
+
},
|
|
9197
|
+
async update(req, id) {
|
|
9198
|
+
const a = await requireAuth(req);
|
|
9199
|
+
if (a) return a;
|
|
9200
|
+
const pe = await perm(req, "update");
|
|
9201
|
+
if (pe) return pe;
|
|
9202
|
+
if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
|
|
9203
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9204
|
+
const existing = await repo.findOne({ where: { id } });
|
|
9205
|
+
if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
|
|
9206
|
+
let body;
|
|
9207
|
+
try {
|
|
9208
|
+
body = await req.json();
|
|
9209
|
+
} catch {
|
|
9210
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9211
|
+
}
|
|
9212
|
+
const patch = parseScheduleBody(body, existing);
|
|
9213
|
+
Object.assign(existing, patch);
|
|
9214
|
+
const validationError = validateScheduleInput(existing);
|
|
9215
|
+
if (validationError) return json({ error: validationError }, { status: 400 });
|
|
9216
|
+
const row = await repo.save(existing);
|
|
9217
|
+
const cms = await getCms();
|
|
9218
|
+
await syncAfterSave(cms, row);
|
|
9219
|
+
return json({ schedule: serializeSchedule(row) });
|
|
9220
|
+
},
|
|
9221
|
+
async remove(req, id) {
|
|
9222
|
+
const a = await requireAuth(req);
|
|
9223
|
+
if (a) return a;
|
|
9224
|
+
const pe = await perm(req, "delete");
|
|
9225
|
+
if (pe) return pe;
|
|
9226
|
+
if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
|
|
9227
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9228
|
+
const existing = await repo.findOne({ where: { id } });
|
|
9229
|
+
if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
|
|
9230
|
+
const cms = await getCms();
|
|
9231
|
+
existing.enabled = false;
|
|
9232
|
+
await syncJobScheduleToPgBoss(cms, existing);
|
|
9233
|
+
await repo.delete({ id });
|
|
9234
|
+
return json({ ok: true });
|
|
9235
|
+
},
|
|
9236
|
+
async runNow(req, id) {
|
|
9237
|
+
const a = await requireAuth(req);
|
|
9238
|
+
if (a) return a;
|
|
9239
|
+
const pe = await perm(req, "create");
|
|
9240
|
+
if (pe) return pe;
|
|
9241
|
+
if (!getCms) return json({ error: "CMS not configured" }, { status: 503 });
|
|
9242
|
+
const repo = dataSource.getRepository(ScheduleEntity);
|
|
9243
|
+
const existing = await repo.findOne({ where: { id } });
|
|
9244
|
+
if (!existing) return json({ error: "Schedule not found" }, { status: 404 });
|
|
9245
|
+
try {
|
|
9246
|
+
const cms = await getCms();
|
|
9247
|
+
await queueJobScheduleNow(cms, id);
|
|
9248
|
+
return json({ ok: true, message: "Job queued" });
|
|
9249
|
+
} catch (e) {
|
|
9250
|
+
const message = e instanceof Error ? e.message : "Failed to queue job";
|
|
9251
|
+
return json({ error: message }, { status: 503 });
|
|
9252
|
+
}
|
|
9253
|
+
},
|
|
9254
|
+
async listRuns(req, id) {
|
|
9255
|
+
const a = await requireAuth(req);
|
|
9256
|
+
if (a) return a;
|
|
9257
|
+
const pe = await perm(req, "read");
|
|
9258
|
+
if (pe) return pe;
|
|
9259
|
+
const runs = await dataSource.getRepository(RunEntity).find({
|
|
9260
|
+
where: { scheduleId: id },
|
|
9261
|
+
order: { startedAt: "DESC" },
|
|
9262
|
+
take: 50
|
|
9263
|
+
});
|
|
9264
|
+
return json({ runs: runs.map(serializeRun) });
|
|
9265
|
+
}
|
|
9266
|
+
};
|
|
9267
|
+
}
|
|
9268
|
+
|
|
8604
9269
|
// src/api/cms-api-handler.ts
|
|
8605
9270
|
var KNOWLEDGE_SUFFIX = "knowledge";
|
|
8606
9271
|
var CMS_API_LOG = "[cms-api]";
|
|
@@ -8661,7 +9326,9 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
8661
9326
|
"message_templates",
|
|
8662
9327
|
"llm_agent_knowledge_documents",
|
|
8663
9328
|
"rss_feeds",
|
|
8664
|
-
"rss_articles"
|
|
9329
|
+
"rss_articles",
|
|
9330
|
+
"job_schedules",
|
|
9331
|
+
"job_schedule_runs"
|
|
8665
9332
|
]);
|
|
8666
9333
|
function createCmsApiHandler(config) {
|
|
8667
9334
|
const {
|
|
@@ -8824,6 +9491,15 @@ function createCmsApiHandler(config) {
|
|
|
8824
9491
|
...llmAgentKnowledgeMerged,
|
|
8825
9492
|
requireEntityPermission: requireEntityPermissionEffective
|
|
8826
9493
|
}) : null;
|
|
9494
|
+
const jobScheduleHandlers = getCms ? createJobScheduleHandlers({
|
|
9495
|
+
dataSource,
|
|
9496
|
+
entityMap,
|
|
9497
|
+
json: config.json,
|
|
9498
|
+
requireAuth: config.requireAuth,
|
|
9499
|
+
requireEntityPermission: requireEntityPermissionEffective,
|
|
9500
|
+
getCms,
|
|
9501
|
+
config: typeof process !== "undefined" ? process.env : {}
|
|
9502
|
+
}) : null;
|
|
8827
9503
|
function resolveResource(segment) {
|
|
8828
9504
|
const model = pathToModel(segment);
|
|
8829
9505
|
return crudResources.includes(model) ? model : segment;
|
|
@@ -8854,6 +9530,30 @@ function createCmsApiHandler(config) {
|
|
|
8854
9530
|
if (g) return g;
|
|
8855
9531
|
return ecommerceAnalyticsGet(req);
|
|
8856
9532
|
}
|
|
9533
|
+
if (path2[0] === "job-schedules" && jobScheduleHandlers) {
|
|
9534
|
+
if (path2.length === 2 && m === "GET") {
|
|
9535
|
+
return jobScheduleHandlers.getById(req, path2[1]);
|
|
9536
|
+
}
|
|
9537
|
+
if (path2.length === 2 && m === "PATCH") {
|
|
9538
|
+
return jobScheduleHandlers.update(req, path2[1]);
|
|
9539
|
+
}
|
|
9540
|
+
if (path2.length === 2 && m === "DELETE") {
|
|
9541
|
+
return jobScheduleHandlers.remove(req, path2[1]);
|
|
9542
|
+
}
|
|
9543
|
+
if (path2.length === 3 && path2[2] === "run-now" && m === "POST") {
|
|
9544
|
+
return jobScheduleHandlers.runNow(req, path2[1]);
|
|
9545
|
+
}
|
|
9546
|
+
if (path2.length === 3 && path2[2] === "runs" && m === "GET") {
|
|
9547
|
+
return jobScheduleHandlers.listRuns(req, path2[1]);
|
|
9548
|
+
}
|
|
9549
|
+
if (path2.length === 1 && m === "GET") {
|
|
9550
|
+
return jobScheduleHandlers.list(req);
|
|
9551
|
+
}
|
|
9552
|
+
if (path2.length === 1 && m === "POST") {
|
|
9553
|
+
return jobScheduleHandlers.create(req);
|
|
9554
|
+
}
|
|
9555
|
+
return config.json({ error: "Not found" }, { status: 404 });
|
|
9556
|
+
}
|
|
8857
9557
|
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 3 && m === "DELETE" && getCms) {
|
|
8858
9558
|
const id = path2[2];
|
|
8859
9559
|
const uuidLooksValid = typeof id === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id);
|
|
@@ -9319,6 +10019,9 @@ function createCmsApiHandler(config) {
|
|
|
9319
10019
|
if (tail === "sync-profile" && m === "POST") {
|
|
9320
10020
|
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
9321
10021
|
}
|
|
10022
|
+
if (tail === "fetch-organizations" && m === "POST") {
|
|
10023
|
+
return socialMediaHandlers.fetchLinkedInOrganizations(req);
|
|
10024
|
+
}
|
|
9322
10025
|
if (tail === "publish-blog" && m === "POST") {
|
|
9323
10026
|
let body;
|
|
9324
10027
|
try {
|
|
@@ -9499,7 +10202,7 @@ function createCmsApiHandler(config) {
|
|
|
9499
10202
|
}
|
|
9500
10203
|
|
|
9501
10204
|
// src/api/storefront-handlers.ts
|
|
9502
|
-
var
|
|
10205
|
+
var import_typeorm52 = require("typeorm");
|
|
9503
10206
|
|
|
9504
10207
|
// src/lib/is-valid-signup-email.ts
|
|
9505
10208
|
var MAX_EMAIL = 254;
|
|
@@ -9758,8 +10461,8 @@ async function queueSms(cms, payload) {
|
|
|
9758
10461
|
}
|
|
9759
10462
|
|
|
9760
10463
|
// src/lib/otp-challenge.ts
|
|
9761
|
-
var
|
|
9762
|
-
var
|
|
10464
|
+
var import_crypto2 = require("crypto");
|
|
10465
|
+
var import_typeorm51 = require("typeorm");
|
|
9763
10466
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
9764
10467
|
var MAX_SENDS_PER_HOUR = 5;
|
|
9765
10468
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -9767,19 +10470,19 @@ function getPepper(explicit) {
|
|
|
9767
10470
|
return (explicit || process.env.OTP_PEPPER || process.env.NEXTAUTH_SECRET || "dev-otp-pepper").trim();
|
|
9768
10471
|
}
|
|
9769
10472
|
function hashOtpCode(code, purpose, identifier, pepper) {
|
|
9770
|
-
return (0,
|
|
10473
|
+
return (0, import_crypto2.createHmac)("sha256", getPepper(pepper)).update(`${purpose}|${identifier}|${code}`).digest("hex");
|
|
9771
10474
|
}
|
|
9772
10475
|
function verifyOtpCodeHash(code, storedHash, purpose, identifier, pepper) {
|
|
9773
10476
|
const h = hashOtpCode(code, purpose, identifier, pepper);
|
|
9774
10477
|
try {
|
|
9775
|
-
return (0,
|
|
10478
|
+
return (0, import_crypto2.timingSafeEqual)(Buffer.from(h, "utf8"), Buffer.from(storedHash, "utf8"));
|
|
9776
10479
|
} catch {
|
|
9777
10480
|
return false;
|
|
9778
10481
|
}
|
|
9779
10482
|
}
|
|
9780
10483
|
function generateNumericOtp(length = 6) {
|
|
9781
10484
|
const max = 10 ** length;
|
|
9782
|
-
return (0,
|
|
10485
|
+
return (0, import_crypto2.randomInt)(0, max).toString().padStart(length, "0");
|
|
9783
10486
|
}
|
|
9784
10487
|
function normalizePhoneE164(raw, defaultCountryCode) {
|
|
9785
10488
|
const t = raw.trim();
|
|
@@ -9793,7 +10496,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
|
|
|
9793
10496
|
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
9794
10497
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
9795
10498
|
return repo.count({
|
|
9796
|
-
where: { purpose, identifier, createdAt: (0,
|
|
10499
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm51.MoreThan)(since) }
|
|
9797
10500
|
});
|
|
9798
10501
|
}
|
|
9799
10502
|
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
@@ -9807,7 +10510,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
9807
10510
|
await repo.delete({
|
|
9808
10511
|
purpose,
|
|
9809
10512
|
identifier,
|
|
9810
|
-
consumedAt: (0,
|
|
10513
|
+
consumedAt: (0, import_typeorm51.IsNull)()
|
|
9811
10514
|
});
|
|
9812
10515
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
9813
10516
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -9828,7 +10531,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
9828
10531
|
const { purpose, identifier, code, pepper } = input;
|
|
9829
10532
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
9830
10533
|
const row = await repo.findOne({
|
|
9831
|
-
where: { purpose, identifier, consumedAt: (0,
|
|
10534
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm51.IsNull)() },
|
|
9832
10535
|
order: { id: "DESC" }
|
|
9833
10536
|
});
|
|
9834
10537
|
if (!row) {
|
|
@@ -10062,7 +10765,7 @@ function createStorefrontApiHandler(config) {
|
|
|
10062
10765
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
10063
10766
|
if (!u) return null;
|
|
10064
10767
|
const unclaimed = await contactRepo().findOne({
|
|
10065
|
-
where: { email: u.email, userId: (0,
|
|
10768
|
+
where: { email: u.email, userId: (0, import_typeorm52.IsNull)(), deleted: false }
|
|
10066
10769
|
});
|
|
10067
10770
|
if (unclaimed) {
|
|
10068
10771
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -11113,7 +11816,7 @@ function createStorefrontApiHandler(config) {
|
|
|
11113
11816
|
const previewByOrder = {};
|
|
11114
11817
|
if (orderIds.length) {
|
|
11115
11818
|
const oItems = await orderItemRepo().find({
|
|
11116
|
-
where: { orderId: (0,
|
|
11819
|
+
where: { orderId: (0, import_typeorm52.In)(orderIds) },
|
|
11117
11820
|
relations: ["product"],
|
|
11118
11821
|
order: { id: "ASC" }
|
|
11119
11822
|
});
|
|
@@ -11250,6 +11953,7 @@ function createStorefrontApiHandler(config) {
|
|
|
11250
11953
|
createForgotPasswordHandler,
|
|
11251
11954
|
createFormBySlugHandler,
|
|
11252
11955
|
createInviteAcceptHandler,
|
|
11956
|
+
createJobScheduleHandlers,
|
|
11253
11957
|
createLlmAgentKnowledgeHandlers,
|
|
11254
11958
|
createMediaZipExtractHandler,
|
|
11255
11959
|
createSetPasswordHandler,
|