@infuro/cms-core 1.0.25 → 1.0.27
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 +6318 -4730
- package/dist/admin.cjs.map +1 -1
- package/dist/admin.d.cts +3 -1
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +5443 -3852
- package/dist/admin.js.map +1 -1
- package/dist/api.cjs +1922 -136
- 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 +1928 -129
- package/dist/api.js.map +1 -1
- package/dist/cli.cjs +23 -5
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +23 -5
- package/dist/cli.js.map +1 -1
- package/dist/{index--vbixpxE.d.cts → index-CI6J9dxr.d.cts} +3 -1
- package/dist/{index-DMJgi-fy.d.ts → index-CMJZ5Fpr.d.ts} +3 -1
- package/dist/index.cjs +3538 -1057
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +333 -4
- package/dist/index.d.ts +333 -4
- package/dist/index.js +3499 -1031
- package/dist/index.js.map +1 -1
- package/dist/migrations/1775500000000-RssFeedsAndArticles.ts +68 -0
- package/dist/migrations/1775600000000-BlogSocialMediaContent.ts +48 -0
- package/package.json +2 -1
package/dist/api.cjs
CHANGED
|
@@ -521,6 +521,8 @@ __export(api_exports, {
|
|
|
521
521
|
getPublicSettingsGroup: () => getPublicSettingsGroup,
|
|
522
522
|
mergeGuardrailsIntoSystemPrompt: () => mergeGuardrailsIntoSystemPrompt,
|
|
523
523
|
parseLlmAgentValidationRules: () => parseLlmAgentValidationRules,
|
|
524
|
+
simpleDecrypt: () => simpleDecrypt,
|
|
525
|
+
simpleEncrypt: () => simpleEncrypt,
|
|
524
526
|
validateUserMessageAgainstAgentRules: () => validateUserMessageAgainstAgentRules,
|
|
525
527
|
validateUserMessageAgainstStructuredRules: () => validateUserMessageAgainstStructuredRules
|
|
526
528
|
});
|
|
@@ -1959,14 +1961,14 @@ function createUserAuthApiRouter(config) {
|
|
|
1959
1961
|
}) : null;
|
|
1960
1962
|
return {
|
|
1961
1963
|
async POST(req, pathname) {
|
|
1962
|
-
const
|
|
1963
|
-
if (!USER_AUTH_PATHS.includes(
|
|
1964
|
+
const path2 = pathname.replace(/\/$/, "");
|
|
1965
|
+
if (!USER_AUTH_PATHS.includes(path2)) {
|
|
1964
1966
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
1965
1967
|
}
|
|
1966
|
-
if (
|
|
1967
|
-
if (
|
|
1968
|
-
if (
|
|
1969
|
-
if (
|
|
1968
|
+
if (path2 === "forgot-password") return forgot(req);
|
|
1969
|
+
if (path2 === "set-password") return setPass(req);
|
|
1970
|
+
if (path2 === "invite") return invite(req);
|
|
1971
|
+
if (path2 === "reset-password" && changePass) return changePass(req);
|
|
1970
1972
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
1971
1973
|
}
|
|
1972
1974
|
};
|
|
@@ -2235,12 +2237,12 @@ async function extractZipMediaIntoParentTree(opts) {
|
|
|
2235
2237
|
if (opts.storage) {
|
|
2236
2238
|
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
2237
2239
|
} else {
|
|
2238
|
-
const
|
|
2240
|
+
const fs2 = await import("fs/promises");
|
|
2239
2241
|
const pathMod = await import("path");
|
|
2240
2242
|
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
2241
2243
|
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
2242
|
-
await
|
|
2243
|
-
await
|
|
2244
|
+
await fs2.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
2245
|
+
await fs2.writeFile(filePath, buf);
|
|
2244
2246
|
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
2245
2247
|
}
|
|
2246
2248
|
await repo.save(
|
|
@@ -2662,12 +2664,12 @@ function createUploadHandler(config) {
|
|
|
2662
2664
|
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
2663
2665
|
return json({ filePath: fileUrl, parentId });
|
|
2664
2666
|
}
|
|
2665
|
-
const
|
|
2666
|
-
const
|
|
2667
|
-
const dir =
|
|
2668
|
-
const filePath =
|
|
2669
|
-
await
|
|
2670
|
-
await
|
|
2667
|
+
const fs2 = await import("fs/promises");
|
|
2668
|
+
const path2 = await import("path");
|
|
2669
|
+
const dir = path2.join(process.cwd(), localUploadDir);
|
|
2670
|
+
const filePath = path2.join(dir, relativeUnderUploads);
|
|
2671
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
2672
|
+
await fs2.writeFile(filePath, buffer);
|
|
2671
2673
|
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
2672
2674
|
return json({ filePath: `/${urlRel}`, parentId });
|
|
2673
2675
|
} catch (err) {
|
|
@@ -3321,11 +3323,11 @@ function createUserAvatarHandler(config) {
|
|
|
3321
3323
|
const ext = file.name.split(".").pop() || "jpg";
|
|
3322
3324
|
const fileName = `avatar_${session.user.email}_${Date.now()}.${ext}`;
|
|
3323
3325
|
const avatarUrl = saveAvatar ? await saveAvatar(buffer, fileName) : await (async () => {
|
|
3324
|
-
const
|
|
3325
|
-
const
|
|
3326
|
-
const dir =
|
|
3327
|
-
await
|
|
3328
|
-
await
|
|
3326
|
+
const fs2 = await import("fs/promises");
|
|
3327
|
+
const path2 = await import("path");
|
|
3328
|
+
const dir = path2.join(process.cwd(), "public", "uploads", "avatars");
|
|
3329
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
3330
|
+
await fs2.writeFile(path2.join(dir, fileName), buffer);
|
|
3329
3331
|
return `/uploads/avatars/${fileName}`;
|
|
3330
3332
|
})();
|
|
3331
3333
|
return json({ message: "Avatar uploaded successfully", avatarUrl });
|
|
@@ -5212,6 +5214,7 @@ var Blog = class {
|
|
|
5212
5214
|
id;
|
|
5213
5215
|
title;
|
|
5214
5216
|
content;
|
|
5217
|
+
socialMediaContent;
|
|
5215
5218
|
coverImage;
|
|
5216
5219
|
authorId;
|
|
5217
5220
|
categoryId;
|
|
@@ -5240,6 +5243,9 @@ __decorateClass([
|
|
|
5240
5243
|
__decorateClass([
|
|
5241
5244
|
(0, import_typeorm16.Column)("text")
|
|
5242
5245
|
], Blog.prototype, "content", 2);
|
|
5246
|
+
__decorateClass([
|
|
5247
|
+
(0, import_typeorm16.Column)("text", { nullable: true })
|
|
5248
|
+
], Blog.prototype, "socialMediaContent", 2);
|
|
5243
5249
|
__decorateClass([
|
|
5244
5250
|
(0, import_typeorm16.Column)("varchar", { nullable: true })
|
|
5245
5251
|
], Blog.prototype, "coverImage", 2);
|
|
@@ -5929,7 +5935,7 @@ __decorateClass([
|
|
|
5929
5935
|
(0, import_typeorm25.JoinColumn)({ name: "userId" })
|
|
5930
5936
|
], Contact.prototype, "user", 2);
|
|
5931
5937
|
__decorateClass([
|
|
5932
|
-
(0, import_typeorm25.OneToMany)(() => FormSubmission, (
|
|
5938
|
+
(0, import_typeorm25.OneToMany)(() => FormSubmission, (fs2) => fs2.contact)
|
|
5933
5939
|
], Contact.prototype, "form_submissions", 2);
|
|
5934
5940
|
__decorateClass([
|
|
5935
5941
|
(0, import_typeorm25.OneToMany)(() => Address, (a) => a.contact)
|
|
@@ -7225,6 +7231,130 @@ LlmAgentKnowledgeDocument = __decorateClass([
|
|
|
7225
7231
|
(0, import_typeorm45.Index)("IDX_llm_agent_knowledge_agent", ["agentId"])
|
|
7226
7232
|
], LlmAgentKnowledgeDocument);
|
|
7227
7233
|
|
|
7234
|
+
// src/entities/rss-feed.entity.ts
|
|
7235
|
+
var import_typeorm47 = require("typeorm");
|
|
7236
|
+
|
|
7237
|
+
// src/entities/rss-article.entity.ts
|
|
7238
|
+
var import_typeorm46 = require("typeorm");
|
|
7239
|
+
var RssArticle = class {
|
|
7240
|
+
id;
|
|
7241
|
+
rssFeedId;
|
|
7242
|
+
rssFeed;
|
|
7243
|
+
externalId;
|
|
7244
|
+
title;
|
|
7245
|
+
articleUrl;
|
|
7246
|
+
summary;
|
|
7247
|
+
content;
|
|
7248
|
+
author;
|
|
7249
|
+
publishedAt;
|
|
7250
|
+
imageUrl;
|
|
7251
|
+
rawData;
|
|
7252
|
+
contentHash;
|
|
7253
|
+
isProcessed;
|
|
7254
|
+
createdAt;
|
|
7255
|
+
};
|
|
7256
|
+
__decorateClass([
|
|
7257
|
+
(0, import_typeorm46.PrimaryGeneratedColumn)("uuid")
|
|
7258
|
+
], RssArticle.prototype, "id", 2);
|
|
7259
|
+
__decorateClass([
|
|
7260
|
+
(0, import_typeorm46.Column)("uuid")
|
|
7261
|
+
], RssArticle.prototype, "rssFeedId", 2);
|
|
7262
|
+
__decorateClass([
|
|
7263
|
+
(0, import_typeorm46.ManyToOne)(() => RssFeed, (f) => f.articles, { onDelete: "CASCADE" }),
|
|
7264
|
+
(0, import_typeorm46.JoinColumn)({ name: "rssFeedId" })
|
|
7265
|
+
], RssArticle.prototype, "rssFeed", 2);
|
|
7266
|
+
__decorateClass([
|
|
7267
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7268
|
+
], RssArticle.prototype, "externalId", 2);
|
|
7269
|
+
__decorateClass([
|
|
7270
|
+
(0, import_typeorm46.Column)({ type: "text" })
|
|
7271
|
+
], RssArticle.prototype, "title", 2);
|
|
7272
|
+
__decorateClass([
|
|
7273
|
+
(0, import_typeorm46.Column)({ type: "text" })
|
|
7274
|
+
], RssArticle.prototype, "articleUrl", 2);
|
|
7275
|
+
__decorateClass([
|
|
7276
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7277
|
+
], RssArticle.prototype, "summary", 2);
|
|
7278
|
+
__decorateClass([
|
|
7279
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7280
|
+
], RssArticle.prototype, "content", 2);
|
|
7281
|
+
__decorateClass([
|
|
7282
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7283
|
+
], RssArticle.prototype, "author", 2);
|
|
7284
|
+
__decorateClass([
|
|
7285
|
+
(0, import_typeorm46.Column)({ type: "timestamp", nullable: true })
|
|
7286
|
+
], RssArticle.prototype, "publishedAt", 2);
|
|
7287
|
+
__decorateClass([
|
|
7288
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7289
|
+
], RssArticle.prototype, "imageUrl", 2);
|
|
7290
|
+
__decorateClass([
|
|
7291
|
+
(0, import_typeorm46.Column)({ type: "jsonb", nullable: true })
|
|
7292
|
+
], RssArticle.prototype, "rawData", 2);
|
|
7293
|
+
__decorateClass([
|
|
7294
|
+
(0, import_typeorm46.Column)({ type: "text", nullable: true })
|
|
7295
|
+
], RssArticle.prototype, "contentHash", 2);
|
|
7296
|
+
__decorateClass([
|
|
7297
|
+
(0, import_typeorm46.Column)({ type: "boolean", default: false })
|
|
7298
|
+
], RssArticle.prototype, "isProcessed", 2);
|
|
7299
|
+
__decorateClass([
|
|
7300
|
+
(0, import_typeorm46.CreateDateColumn)()
|
|
7301
|
+
], RssArticle.prototype, "createdAt", 2);
|
|
7302
|
+
RssArticle = __decorateClass([
|
|
7303
|
+
(0, import_typeorm46.Entity)("rss_articles"),
|
|
7304
|
+
(0, import_typeorm46.Unique)("UQ_rss_articles_feed_article_url", ["rssFeedId", "articleUrl"])
|
|
7305
|
+
], RssArticle);
|
|
7306
|
+
|
|
7307
|
+
// src/entities/rss-feed.entity.ts
|
|
7308
|
+
var RssFeed = class {
|
|
7309
|
+
id;
|
|
7310
|
+
name;
|
|
7311
|
+
rssUrl;
|
|
7312
|
+
websiteUrl;
|
|
7313
|
+
isActive;
|
|
7314
|
+
fetchFrequencyMinutes;
|
|
7315
|
+
lastFetchedAt;
|
|
7316
|
+
lastArticleDate;
|
|
7317
|
+
articles;
|
|
7318
|
+
createdAt;
|
|
7319
|
+
updatedAt;
|
|
7320
|
+
};
|
|
7321
|
+
__decorateClass([
|
|
7322
|
+
(0, import_typeorm47.PrimaryGeneratedColumn)("uuid")
|
|
7323
|
+
], RssFeed.prototype, "id", 2);
|
|
7324
|
+
__decorateClass([
|
|
7325
|
+
(0, import_typeorm47.Column)({ type: "text" })
|
|
7326
|
+
], RssFeed.prototype, "name", 2);
|
|
7327
|
+
__decorateClass([
|
|
7328
|
+
(0, import_typeorm47.Column)({ type: "text", unique: true })
|
|
7329
|
+
], RssFeed.prototype, "rssUrl", 2);
|
|
7330
|
+
__decorateClass([
|
|
7331
|
+
(0, import_typeorm47.Column)({ type: "text", nullable: true })
|
|
7332
|
+
], RssFeed.prototype, "websiteUrl", 2);
|
|
7333
|
+
__decorateClass([
|
|
7334
|
+
(0, import_typeorm47.Column)({ type: "boolean", default: true })
|
|
7335
|
+
], RssFeed.prototype, "isActive", 2);
|
|
7336
|
+
__decorateClass([
|
|
7337
|
+
(0, import_typeorm47.Column)({ type: "int", default: 60 })
|
|
7338
|
+
], RssFeed.prototype, "fetchFrequencyMinutes", 2);
|
|
7339
|
+
__decorateClass([
|
|
7340
|
+
(0, import_typeorm47.Column)({ type: "timestamp", nullable: true })
|
|
7341
|
+
], RssFeed.prototype, "lastFetchedAt", 2);
|
|
7342
|
+
__decorateClass([
|
|
7343
|
+
(0, import_typeorm47.Column)({ type: "timestamp", nullable: true })
|
|
7344
|
+
], RssFeed.prototype, "lastArticleDate", 2);
|
|
7345
|
+
__decorateClass([
|
|
7346
|
+
(0, import_typeorm47.OneToMany)(() => RssArticle, (article) => article.rssFeed)
|
|
7347
|
+
], RssFeed.prototype, "articles", 2);
|
|
7348
|
+
__decorateClass([
|
|
7349
|
+
(0, import_typeorm47.CreateDateColumn)()
|
|
7350
|
+
], RssFeed.prototype, "createdAt", 2);
|
|
7351
|
+
__decorateClass([
|
|
7352
|
+
(0, import_typeorm47.UpdateDateColumn)()
|
|
7353
|
+
], RssFeed.prototype, "updatedAt", 2);
|
|
7354
|
+
RssFeed = __decorateClass([
|
|
7355
|
+
(0, import_typeorm47.Entity)("rss_feeds")
|
|
7356
|
+
], RssFeed);
|
|
7357
|
+
|
|
7228
7358
|
// src/entities/index.ts
|
|
7229
7359
|
var CMS_ENTITY_MAP = {
|
|
7230
7360
|
users: User,
|
|
@@ -7266,9 +7396,1211 @@ var CMS_ENTITY_MAP = {
|
|
|
7266
7396
|
wishlists: Wishlist,
|
|
7267
7397
|
wishlist_items: WishlistItem,
|
|
7268
7398
|
llm_agents: LlmAgent,
|
|
7269
|
-
llm_agent_knowledge_documents: LlmAgentKnowledgeDocument
|
|
7399
|
+
llm_agent_knowledge_documents: LlmAgentKnowledgeDocument,
|
|
7400
|
+
rss_feeds: RssFeed,
|
|
7401
|
+
rss_articles: RssArticle
|
|
7270
7402
|
};
|
|
7271
7403
|
|
|
7404
|
+
// src/plugins/blog-generator/blog-generator-service.ts
|
|
7405
|
+
var import_rss_parser = __toESM(require("rss-parser"), 1);
|
|
7406
|
+
|
|
7407
|
+
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
7408
|
+
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
7409
|
+
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
7410
|
+
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
7411
|
+
|
|
7412
|
+
Your task is to generate completely original, high-quality blog article(s) from the latest RSS-derived source material in the user message (one block per feed). The user message is factual input only.
|
|
7413
|
+
|
|
7414
|
+
IMPORTANT RULES:
|
|
7415
|
+
|
|
7416
|
+
1. NEVER copy the RSS article directly.
|
|
7417
|
+
2. Rewrite the information into a fresh, human-like article.
|
|
7418
|
+
3. Expand the topic with professional insights and explanations.
|
|
7419
|
+
4. Maintain a natural editorial tone.
|
|
7420
|
+
5. Make the article SEO-friendly.
|
|
7421
|
+
6. Use engaging headings and subheadings.
|
|
7422
|
+
7. Avoid robotic AI phrasing.
|
|
7423
|
+
8. Do not mention that the content came from RSS or feeds.
|
|
7424
|
+
9. Preserve factual accuracy from the source material.
|
|
7425
|
+
10. The output should feel like a professionally written industry blog.
|
|
7426
|
+
|
|
7427
|
+
MULTIPLE FEEDS:
|
|
7428
|
+
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
7429
|
+
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
7430
|
+
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
7431
|
+
|
|
7432
|
+
WRITING STYLE:
|
|
7433
|
+
- Professional
|
|
7434
|
+
- Clear
|
|
7435
|
+
- Informative
|
|
7436
|
+
- Business-focused
|
|
7437
|
+
- Human sounding
|
|
7438
|
+
- Modern editorial style
|
|
7439
|
+
|
|
7440
|
+
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
7441
|
+
1. Engaging introduction
|
|
7442
|
+
2. Industry context
|
|
7443
|
+
3. Main developments
|
|
7444
|
+
4. Key implications
|
|
7445
|
+
5. Expert/business analysis
|
|
7446
|
+
6. Conclusion
|
|
7447
|
+
|
|
7448
|
+
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
7449
|
+
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
7450
|
+
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
7451
|
+
- Inside <article>, use this outline:
|
|
7452
|
+
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
7453
|
+
- <section class="blog-post-body"> for all following content
|
|
7454
|
+
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
7455
|
+
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
7456
|
+
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
7457
|
+
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
7458
|
+
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
7459
|
+
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
7460
|
+
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
7461
|
+
|
|
7462
|
+
CONTENT REQUIREMENTS:
|
|
7463
|
+
- Minimum 800 words per article unless source material is very small.
|
|
7464
|
+
- Add context around industry trends where appropriate.
|
|
7465
|
+
- Explain why the topic matters.
|
|
7466
|
+
- Include practical implications for businesses/professionals.
|
|
7467
|
+
|
|
7468
|
+
IF RSS CONTENT IS LIMITED:
|
|
7469
|
+
- Expand intelligently using general industry knowledge.
|
|
7470
|
+
- Keep the article relevant to the original topic.
|
|
7471
|
+
- Do not invent fake facts or statistics.
|
|
7472
|
+
|
|
7473
|
+
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
7474
|
+
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
7475
|
+
- If you output more than one article, output multiple <article class="blog-post">\u2026</article> blocks in sequence, and between articles put a single line containing exactly this (and nothing else on that line):
|
|
7476
|
+
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
7477
|
+
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
7478
|
+
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7479
|
+
{
|
|
7480
|
+
maxUserChars: 5e5,
|
|
7481
|
+
guardrails: `Your reply must be semantic HTML blog article(s) only, as in the system OUTPUT FORMAT (<article class="blog-post">\u2026). No JSON, no Markdown, no explanations before or after the HTML. If multiple articles, separate them with a line containing exactly ${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR} alone. Do not mention RSS, feeds, or scraping. Preserve factual accuracy; do not invent statistics or quotes.`
|
|
7482
|
+
},
|
|
7483
|
+
null,
|
|
7484
|
+
2
|
|
7485
|
+
);
|
|
7486
|
+
|
|
7487
|
+
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
7488
|
+
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
7489
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7490
|
+
{
|
|
7491
|
+
maxUserChars: 5e5,
|
|
7492
|
+
guardrails: "Reply with one valid JSON object only (see system OUTPUT FORMAT). No markdown fences, no text before or after JSON. Keys: categoryName, blogSlug, seo { title, description, keywords, ogTitle, ogDescription }, tags (array of strings)."
|
|
7493
|
+
},
|
|
7494
|
+
null,
|
|
7495
|
+
2
|
|
7496
|
+
);
|
|
7497
|
+
|
|
7498
|
+
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
7499
|
+
var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
|
|
7500
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7501
|
+
{
|
|
7502
|
+
maxUserChars: 5e5,
|
|
7503
|
+
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
7504
|
+
},
|
|
7505
|
+
null,
|
|
7506
|
+
2
|
|
7507
|
+
);
|
|
7508
|
+
|
|
7509
|
+
// src/plugins/blog-generator/blog-generator-service.ts
|
|
7510
|
+
var parser = new import_rss_parser.default({
|
|
7511
|
+
defaultRSS: 2
|
|
7512
|
+
});
|
|
7513
|
+
function resolveBlogCategoryIdByName(needle, rows) {
|
|
7514
|
+
if (!needle?.trim()) return { categoryId: null, matched: false };
|
|
7515
|
+
const n = needle.trim().toLowerCase();
|
|
7516
|
+
const hit = rows.find((r) => r.name.trim().toLowerCase() === n);
|
|
7517
|
+
return hit ? { categoryId: hit.id, matched: true } : { categoryId: null, matched: false };
|
|
7518
|
+
}
|
|
7519
|
+
|
|
7520
|
+
// src/plugins/blog-generator/blog-generator-persist.ts
|
|
7521
|
+
function slugBase(draft) {
|
|
7522
|
+
const raw = (draft.slug?.trim() || draft.title || "post").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
7523
|
+
return raw || "post";
|
|
7524
|
+
}
|
|
7525
|
+
function normalizeSlugSegment(input) {
|
|
7526
|
+
const t = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
7527
|
+
return t || "post";
|
|
7528
|
+
}
|
|
7529
|
+
async function allocateUniqueSlug(repo, base) {
|
|
7530
|
+
let b = normalizeSlugSegment(base);
|
|
7531
|
+
for (let i = 0; i < 120; i++) {
|
|
7532
|
+
const s = i === 0 ? b : `${b}-${i + 1}`.slice(0, 160);
|
|
7533
|
+
const exists = await repo.findOne({ where: { slug: s } });
|
|
7534
|
+
if (!exists) return s;
|
|
7535
|
+
}
|
|
7536
|
+
throw new Error("Could not allocate a unique slug");
|
|
7537
|
+
}
|
|
7538
|
+
async function findOrCreateTag(repo, name) {
|
|
7539
|
+
const trimmed = name.trim().slice(0, 500);
|
|
7540
|
+
if (!trimmed) throw new Error("Empty tag name");
|
|
7541
|
+
let row = await repo.findOne({ where: { name: trimmed } });
|
|
7542
|
+
if (row && row.deleted) {
|
|
7543
|
+
await repo.update(
|
|
7544
|
+
{ id: row.id },
|
|
7545
|
+
{ deleted: false, updatedAt: /* @__PURE__ */ new Date() }
|
|
7546
|
+
);
|
|
7547
|
+
row = await repo.findOne({ where: { id: row.id } });
|
|
7548
|
+
return row;
|
|
7549
|
+
}
|
|
7550
|
+
if (row) return row;
|
|
7551
|
+
const now = /* @__PURE__ */ new Date();
|
|
7552
|
+
return repo.save(
|
|
7553
|
+
repo.create({
|
|
7554
|
+
name: trimmed,
|
|
7555
|
+
deleted: false,
|
|
7556
|
+
createdAt: now,
|
|
7557
|
+
updatedAt: now
|
|
7558
|
+
})
|
|
7559
|
+
);
|
|
7560
|
+
}
|
|
7561
|
+
async function findOrCreateCategory(repo, displayName) {
|
|
7562
|
+
const n = displayName.trim().slice(0, 500);
|
|
7563
|
+
if (!n) return null;
|
|
7564
|
+
let row = await repo.createQueryBuilder("c").where("LOWER(TRIM(c.name)) = LOWER(TRIM(:n))", { n }).getOne();
|
|
7565
|
+
if (row && row.deleted) {
|
|
7566
|
+
await repo.update(
|
|
7567
|
+
{ id: row.id },
|
|
7568
|
+
{ deleted: false, updatedAt: /* @__PURE__ */ new Date() }
|
|
7569
|
+
);
|
|
7570
|
+
row = await repo.findOne({ where: { id: row.id } });
|
|
7571
|
+
return row;
|
|
7572
|
+
}
|
|
7573
|
+
if (row) return row;
|
|
7574
|
+
const now = /* @__PURE__ */ new Date();
|
|
7575
|
+
return repo.save(
|
|
7576
|
+
repo.create({
|
|
7577
|
+
name: n,
|
|
7578
|
+
deleted: false,
|
|
7579
|
+
createdAt: now,
|
|
7580
|
+
updatedAt: now
|
|
7581
|
+
})
|
|
7582
|
+
);
|
|
7583
|
+
}
|
|
7584
|
+
async function persistGeneratedBlogDraft(manager, maps, params) {
|
|
7585
|
+
const { draft, authorId } = params;
|
|
7586
|
+
const blogRepo = manager.getRepository(maps.blogs);
|
|
7587
|
+
const tagRepo = manager.getRepository(maps.tags);
|
|
7588
|
+
const catRepo = manager.getRepository(maps.categories);
|
|
7589
|
+
const seoRepo = manager.getRepository(maps.seos);
|
|
7590
|
+
const uniqueBlogSlug = await allocateUniqueSlug(blogRepo, slugBase(draft));
|
|
7591
|
+
const uniqueSeoSlug = await allocateUniqueSlug(seoRepo, uniqueBlogSlug);
|
|
7592
|
+
const seoRow = await seoRepo.save(
|
|
7593
|
+
seoRepo.create({
|
|
7594
|
+
slug: uniqueSeoSlug,
|
|
7595
|
+
title: draft.seo.title,
|
|
7596
|
+
description: draft.seo.description,
|
|
7597
|
+
keywords: draft.seo.keywords,
|
|
7598
|
+
ogTitle: draft.seo.ogTitle ?? draft.seo.title,
|
|
7599
|
+
ogDescription: draft.seo.ogDescription ?? draft.seo.description,
|
|
7600
|
+
ogImage: null,
|
|
7601
|
+
deleted: false,
|
|
7602
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
7603
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7604
|
+
})
|
|
7605
|
+
);
|
|
7606
|
+
const seoId = Number(seoRow.id);
|
|
7607
|
+
let categoryId = null;
|
|
7608
|
+
if (draft.categoryName?.trim()) {
|
|
7609
|
+
const cat = await findOrCreateCategory(catRepo, draft.categoryName);
|
|
7610
|
+
if (cat) categoryId = Number(cat.id);
|
|
7611
|
+
}
|
|
7612
|
+
const tagEntities = [];
|
|
7613
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7614
|
+
for (const t of draft.tags ?? []) {
|
|
7615
|
+
const key = t.trim().toLowerCase();
|
|
7616
|
+
if (!key || seen.has(key)) continue;
|
|
7617
|
+
seen.add(key);
|
|
7618
|
+
tagEntities.push(await findOrCreateTag(tagRepo, t));
|
|
7619
|
+
}
|
|
7620
|
+
const socialTrim = typeof draft.socialMediaContent === "string" && draft.socialMediaContent.trim() ? draft.socialMediaContent.trim().slice(0, 2900) : null;
|
|
7621
|
+
const now = /* @__PURE__ */ new Date();
|
|
7622
|
+
const blogRow = await blogRepo.save(
|
|
7623
|
+
blogRepo.create({
|
|
7624
|
+
title: draft.title.trim().slice(0, 500) || "Untitled",
|
|
7625
|
+
content: draft.markdown,
|
|
7626
|
+
socialMediaContent: socialTrim,
|
|
7627
|
+
slug: uniqueBlogSlug,
|
|
7628
|
+
authorId,
|
|
7629
|
+
categoryId,
|
|
7630
|
+
seoId,
|
|
7631
|
+
published: false,
|
|
7632
|
+
deleted: false,
|
|
7633
|
+
coverImage: null,
|
|
7634
|
+
createdBy: authorId,
|
|
7635
|
+
updatedBy: authorId,
|
|
7636
|
+
createdAt: now,
|
|
7637
|
+
updatedAt: now
|
|
7638
|
+
})
|
|
7639
|
+
);
|
|
7640
|
+
const blogId = Number(blogRow.id);
|
|
7641
|
+
if (tagEntities.length > 0) {
|
|
7642
|
+
const blog = await blogRepo.findOne({
|
|
7643
|
+
where: { id: blogId },
|
|
7644
|
+
relations: ["tags"]
|
|
7645
|
+
});
|
|
7646
|
+
if (blog) {
|
|
7647
|
+
blog.tags = tagEntities;
|
|
7648
|
+
await blogRepo.save(blog);
|
|
7649
|
+
}
|
|
7650
|
+
}
|
|
7651
|
+
const tagIds = tagEntities.map((e) => Number(e.id)).filter((id) => Number.isFinite(id));
|
|
7652
|
+
return { blogId, slug: uniqueBlogSlug, categoryId, seoId, tagIds };
|
|
7653
|
+
}
|
|
7654
|
+
async function persistAllGeneratedBlogDrafts(dataSource, maps, params) {
|
|
7655
|
+
const out = [];
|
|
7656
|
+
for (const draft of params.drafts) {
|
|
7657
|
+
const row = await dataSource.transaction(
|
|
7658
|
+
(manager) => persistGeneratedBlogDraft(manager, maps, { draft, authorId: params.authorId })
|
|
7659
|
+
);
|
|
7660
|
+
out.push(row);
|
|
7661
|
+
}
|
|
7662
|
+
return out;
|
|
7663
|
+
}
|
|
7664
|
+
|
|
7665
|
+
// src/api/rss-feed-blog-api.ts
|
|
7666
|
+
function deriveRssFeedDisplayName(rssUrl) {
|
|
7667
|
+
try {
|
|
7668
|
+
const u = new URL(rssUrl);
|
|
7669
|
+
const h = u.hostname.replace(/^www\./i, "");
|
|
7670
|
+
return (h || rssUrl).slice(0, 500);
|
|
7671
|
+
} catch {
|
|
7672
|
+
return rssUrl.slice(0, 500);
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
async function upsertRssFeedRow(dataSource, feedEntity, rssUrl, opts) {
|
|
7676
|
+
const repo = dataSource.getRepository(feedEntity);
|
|
7677
|
+
const trimmed = rssUrl.trim();
|
|
7678
|
+
const name = (opts?.name?.trim() || deriveRssFeedDisplayName(trimmed)).slice(0, 500);
|
|
7679
|
+
let row = await repo.findOne({ where: { rssUrl: trimmed } });
|
|
7680
|
+
if (!row) {
|
|
7681
|
+
row = repo.create({
|
|
7682
|
+
name,
|
|
7683
|
+
rssUrl: trimmed,
|
|
7684
|
+
websiteUrl: opts?.websiteUrl?.trim() || null,
|
|
7685
|
+
isActive: true,
|
|
7686
|
+
fetchFrequencyMinutes: 60
|
|
7687
|
+
});
|
|
7688
|
+
} else {
|
|
7689
|
+
row.name = name;
|
|
7690
|
+
if (opts?.websiteUrl !== void 0) {
|
|
7691
|
+
row.websiteUrl = opts.websiteUrl?.trim() || null;
|
|
7692
|
+
}
|
|
7693
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
7694
|
+
}
|
|
7695
|
+
return repo.save(row);
|
|
7696
|
+
}
|
|
7697
|
+
async function recordRssFetchSuccess(dataSource, feedEntity, articleEntity, rssUrl, latest) {
|
|
7698
|
+
const feedRepo = dataSource.getRepository(feedEntity);
|
|
7699
|
+
const row = await feedRepo.findOne({ where: { rssUrl: rssUrl.trim() } });
|
|
7700
|
+
if (!row) return;
|
|
7701
|
+
row.lastFetchedAt = /* @__PURE__ */ new Date();
|
|
7702
|
+
if (latest?.date) {
|
|
7703
|
+
row.lastArticleDate = latest.date;
|
|
7704
|
+
}
|
|
7705
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
7706
|
+
await feedRepo.save(row);
|
|
7707
|
+
const articleUrl = latest?.link?.trim();
|
|
7708
|
+
if (!latest || !articleUrl || !articleEntity) return;
|
|
7709
|
+
const artRepo = dataSource.getRepository(articleEntity);
|
|
7710
|
+
await artRepo.upsert(
|
|
7711
|
+
{
|
|
7712
|
+
rssFeedId: row.id,
|
|
7713
|
+
externalId: articleUrl.slice(0, 2e3),
|
|
7714
|
+
title: (latest.title?.trim() || "Untitled").slice(0, 2e3),
|
|
7715
|
+
articleUrl: articleUrl.slice(0, 2e3),
|
|
7716
|
+
summary: latest.summary?.trim() ? latest.summary.trim().slice(0, 1e5) : null,
|
|
7717
|
+
content: latest.content?.trim() ? latest.content.trim().slice(0, 5e5) : null,
|
|
7718
|
+
author: latest.creator?.trim() ? latest.creator.trim().slice(0, 500) : null,
|
|
7719
|
+
publishedAt: latest.date,
|
|
7720
|
+
imageUrl: null,
|
|
7721
|
+
rawData: { source: "blog-generator-latest", rssUrl: rssUrl.trim() },
|
|
7722
|
+
contentHash: null,
|
|
7723
|
+
isProcessed: false
|
|
7724
|
+
},
|
|
7725
|
+
{ conflictPaths: ["rssFeedId", "articleUrl"], skipUpdateIfNoValuesChanged: false }
|
|
7726
|
+
);
|
|
7727
|
+
}
|
|
7728
|
+
function serializeRssFeed(f) {
|
|
7729
|
+
return {
|
|
7730
|
+
id: f.id,
|
|
7731
|
+
name: f.name,
|
|
7732
|
+
rssUrl: f.rssUrl,
|
|
7733
|
+
websiteUrl: f.websiteUrl,
|
|
7734
|
+
isActive: f.isActive,
|
|
7735
|
+
fetchFrequencyMinutes: f.fetchFrequencyMinutes,
|
|
7736
|
+
lastFetchedAt: f.lastFetchedAt?.toISOString() ?? null,
|
|
7737
|
+
lastArticleDate: f.lastArticleDate?.toISOString() ?? null,
|
|
7738
|
+
createdAt: f.createdAt.toISOString(),
|
|
7739
|
+
updatedAt: f.updatedAt.toISOString()
|
|
7740
|
+
};
|
|
7741
|
+
}
|
|
7742
|
+
|
|
7743
|
+
// src/plugins/social-media/social-media-api-handlers.ts
|
|
7744
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
7745
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
7746
|
+
|
|
7747
|
+
// src/plugins/social-media/linkedin-client.ts
|
|
7748
|
+
var RESTLI = "2.0.0";
|
|
7749
|
+
async function linkedInGetUserinfo(accessToken) {
|
|
7750
|
+
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7751
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
7752
|
+
});
|
|
7753
|
+
const text = await r.text();
|
|
7754
|
+
if (!r.ok) {
|
|
7755
|
+
throw new Error(`LinkedIn userinfo failed (${r.status}): ${text.slice(0, 500)}`);
|
|
7756
|
+
}
|
|
7757
|
+
try {
|
|
7758
|
+
return JSON.parse(text);
|
|
7759
|
+
} catch {
|
|
7760
|
+
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7763
|
+
function personUrn(personSub) {
|
|
7764
|
+
const s = String(personSub || "").trim();
|
|
7765
|
+
if (s.startsWith("urn:li:person:")) return s;
|
|
7766
|
+
return `urn:li:person:${s}`;
|
|
7767
|
+
}
|
|
7768
|
+
async function linkedInRegisterImageUpload(accessToken, personSub) {
|
|
7769
|
+
const owner = personUrn(personSub);
|
|
7770
|
+
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7771
|
+
method: "POST",
|
|
7772
|
+
headers: {
|
|
7773
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7774
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7775
|
+
"Content-Type": "application/json"
|
|
7776
|
+
},
|
|
7777
|
+
body: JSON.stringify({
|
|
7778
|
+
registerUploadRequest: {
|
|
7779
|
+
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
7780
|
+
owner,
|
|
7781
|
+
serviceRelationships: [
|
|
7782
|
+
{
|
|
7783
|
+
relationshipType: "OWNER",
|
|
7784
|
+
identifier: "urn:li:userGeneratedContent"
|
|
7785
|
+
}
|
|
7786
|
+
]
|
|
7787
|
+
}
|
|
7788
|
+
})
|
|
7789
|
+
});
|
|
7790
|
+
const text = await r.text();
|
|
7791
|
+
if (!r.ok) {
|
|
7792
|
+
throw new Error(`LinkedIn registerUpload failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7793
|
+
}
|
|
7794
|
+
let data;
|
|
7795
|
+
try {
|
|
7796
|
+
data = JSON.parse(text);
|
|
7797
|
+
} catch {
|
|
7798
|
+
throw new Error("LinkedIn registerUpload: invalid JSON");
|
|
7799
|
+
}
|
|
7800
|
+
const v = data.value;
|
|
7801
|
+
const uploadUrl = v?.uploadMechanism?.["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]?.uploadUrl;
|
|
7802
|
+
const asset = v?.asset;
|
|
7803
|
+
if (!uploadUrl || !asset) {
|
|
7804
|
+
throw new Error("LinkedIn registerUpload: missing uploadUrl or asset");
|
|
7805
|
+
}
|
|
7806
|
+
return { uploadUrl, asset };
|
|
7807
|
+
}
|
|
7808
|
+
async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
7809
|
+
const r = await fetch(uploadUrl, {
|
|
7810
|
+
method: "PUT",
|
|
7811
|
+
headers: {
|
|
7812
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7813
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7814
|
+
"Content-Type": contentType || "application/octet-stream"
|
|
7815
|
+
},
|
|
7816
|
+
body: new Uint8Array(body)
|
|
7817
|
+
});
|
|
7818
|
+
if (!r.ok) {
|
|
7819
|
+
const t = await r.text();
|
|
7820
|
+
throw new Error(`LinkedIn binary upload failed (${r.status}): ${t.slice(0, 500)}`);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7823
|
+
async function linkedInCreateImageShare(params) {
|
|
7824
|
+
const author = personUrn(params.personSub);
|
|
7825
|
+
const commentary = params.commentary.slice(0, 2900);
|
|
7826
|
+
const title = params.title.slice(0, 200);
|
|
7827
|
+
const description = (params.description ?? title).slice(0, 200);
|
|
7828
|
+
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7829
|
+
method: "POST",
|
|
7830
|
+
headers: {
|
|
7831
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
7832
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7833
|
+
"Content-Type": "application/json"
|
|
7834
|
+
},
|
|
7835
|
+
body: JSON.stringify({
|
|
7836
|
+
author,
|
|
7837
|
+
lifecycleState: "PUBLISHED",
|
|
7838
|
+
specificContent: {
|
|
7839
|
+
"com.linkedin.ugc.ShareContent": {
|
|
7840
|
+
shareCommentary: { attributes: [], text: commentary },
|
|
7841
|
+
shareMediaCategory: "IMAGE",
|
|
7842
|
+
media: [
|
|
7843
|
+
{
|
|
7844
|
+
status: "READY",
|
|
7845
|
+
description: { attributes: [], text: description },
|
|
7846
|
+
media: params.asset,
|
|
7847
|
+
title: { attributes: [], text: title }
|
|
7848
|
+
}
|
|
7849
|
+
]
|
|
7850
|
+
}
|
|
7851
|
+
},
|
|
7852
|
+
visibility: {
|
|
7853
|
+
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
|
7854
|
+
}
|
|
7855
|
+
})
|
|
7856
|
+
});
|
|
7857
|
+
const text = await r.text();
|
|
7858
|
+
if (!r.ok) {
|
|
7859
|
+
throw new Error(`LinkedIn ugcPosts failed (${r.status}): ${text.slice(0, 1200)}`);
|
|
7860
|
+
}
|
|
7861
|
+
let data;
|
|
7862
|
+
try {
|
|
7863
|
+
data = JSON.parse(text);
|
|
7864
|
+
} catch {
|
|
7865
|
+
return { status: r.status };
|
|
7866
|
+
}
|
|
7867
|
+
return { status: r.status, id: data.id };
|
|
7868
|
+
}
|
|
7869
|
+
async function linkedInCreateTextShare(params) {
|
|
7870
|
+
const author = personUrn(params.personSub);
|
|
7871
|
+
const commentary = params.commentary.slice(0, 2900);
|
|
7872
|
+
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7873
|
+
method: "POST",
|
|
7874
|
+
headers: {
|
|
7875
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
7876
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7877
|
+
"Content-Type": "application/json"
|
|
7878
|
+
},
|
|
7879
|
+
body: JSON.stringify({
|
|
7880
|
+
author,
|
|
7881
|
+
lifecycleState: "PUBLISHED",
|
|
7882
|
+
specificContent: {
|
|
7883
|
+
"com.linkedin.ugc.ShareContent": {
|
|
7884
|
+
shareCommentary: { attributes: [], text: commentary },
|
|
7885
|
+
shareMediaCategory: "NONE"
|
|
7886
|
+
}
|
|
7887
|
+
},
|
|
7888
|
+
visibility: {
|
|
7889
|
+
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
|
7890
|
+
}
|
|
7891
|
+
})
|
|
7892
|
+
});
|
|
7893
|
+
const text = await r.text();
|
|
7894
|
+
if (!r.ok) {
|
|
7895
|
+
throw new Error(`LinkedIn ugcPosts (text) failed (${r.status}): ${text.slice(0, 1200)}`);
|
|
7896
|
+
}
|
|
7897
|
+
let data;
|
|
7898
|
+
try {
|
|
7899
|
+
data = JSON.parse(text);
|
|
7900
|
+
} catch {
|
|
7901
|
+
return { status: r.status };
|
|
7902
|
+
}
|
|
7903
|
+
return { status: r.status, id: data.id };
|
|
7904
|
+
}
|
|
7905
|
+
|
|
7906
|
+
// src/plugins/social-media/meta-service.ts
|
|
7907
|
+
var GRAPH_API_VERSION = "v23.0";
|
|
7908
|
+
var GRAPH_BASE = `https://graph.facebook.com/${GRAPH_API_VERSION}`;
|
|
7909
|
+
async function metaFetchUserManagedPages(userAccessToken) {
|
|
7910
|
+
const token = String(userAccessToken || "").trim();
|
|
7911
|
+
if (!token) {
|
|
7912
|
+
throw new Error("User access token is required");
|
|
7913
|
+
}
|
|
7914
|
+
const url = new URL(`${GRAPH_BASE}/me/accounts`);
|
|
7915
|
+
url.searchParams.set("access_token", token);
|
|
7916
|
+
const r = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" } });
|
|
7917
|
+
const text = await r.text();
|
|
7918
|
+
let body;
|
|
7919
|
+
try {
|
|
7920
|
+
body = JSON.parse(text);
|
|
7921
|
+
} catch {
|
|
7922
|
+
throw new Error(`Facebook Graph returned invalid JSON (${r.status})`);
|
|
7923
|
+
}
|
|
7924
|
+
if (body.error?.message) {
|
|
7925
|
+
const code = body.error.code != null ? ` (code ${body.error.code})` : "";
|
|
7926
|
+
throw new Error(`${body.error.message}${code}`);
|
|
7927
|
+
}
|
|
7928
|
+
if (!r.ok) {
|
|
7929
|
+
throw new Error(`Facebook Graph request failed (${r.status}): ${text.slice(0, 600)}`);
|
|
7930
|
+
}
|
|
7931
|
+
return body;
|
|
7932
|
+
}
|
|
7933
|
+
function parseGraphMutation(text, httpOk) {
|
|
7934
|
+
let body;
|
|
7935
|
+
try {
|
|
7936
|
+
body = JSON.parse(text);
|
|
7937
|
+
} catch {
|
|
7938
|
+
throw new Error(`Facebook Graph returned invalid JSON (${httpOk ? "200" : "error"})`);
|
|
7939
|
+
}
|
|
7940
|
+
if (body.error?.message) {
|
|
7941
|
+
const code = body.error.code != null ? ` (code ${body.error.code})` : "";
|
|
7942
|
+
throw new Error(`${body.error.message}${code}`);
|
|
7943
|
+
}
|
|
7944
|
+
if (!httpOk) {
|
|
7945
|
+
throw new Error(`Facebook Graph request failed: ${text.slice(0, 600)}`);
|
|
7946
|
+
}
|
|
7947
|
+
return body;
|
|
7948
|
+
}
|
|
7949
|
+
function metaResolvePageAccessToken(accounts, pageId) {
|
|
7950
|
+
const id = String(pageId || "").trim();
|
|
7951
|
+
if (!id || !accounts.data?.length) return null;
|
|
7952
|
+
const row = accounts.data.find((p) => String(p.id ?? "").trim() === id);
|
|
7953
|
+
const tok = row?.access_token;
|
|
7954
|
+
return typeof tok === "string" && tok.trim() ? tok.trim() : null;
|
|
7955
|
+
}
|
|
7956
|
+
async function metaPostPagePhoto(opts) {
|
|
7957
|
+
const { pageId, pageAccessToken, imageBuffer, contentType, caption } = opts;
|
|
7958
|
+
const pid = String(pageId || "").trim();
|
|
7959
|
+
const tok = String(pageAccessToken || "").trim();
|
|
7960
|
+
if (!pid || !tok) throw new Error("Page ID and page access token are required");
|
|
7961
|
+
if (!imageBuffer?.length) throw new Error("Image buffer is empty");
|
|
7962
|
+
const url = `${GRAPH_BASE}/${encodeURIComponent(pid)}/photos`;
|
|
7963
|
+
const lower = contentType.split(";")[0].trim().toLowerCase();
|
|
7964
|
+
const filename = lower.includes("png") ? "photo.png" : "photo.jpg";
|
|
7965
|
+
const blob = new Blob([new Uint8Array(imageBuffer)], { type: lower || "image/jpeg" });
|
|
7966
|
+
const form = new FormData();
|
|
7967
|
+
form.append("access_token", tok);
|
|
7968
|
+
form.append("caption", String(caption || "").trim().slice(0, 2200));
|
|
7969
|
+
form.append("source", blob, filename);
|
|
7970
|
+
const r = await fetch(url, { method: "POST", body: form });
|
|
7971
|
+
const text = await r.text();
|
|
7972
|
+
return parseGraphMutation(text, r.ok);
|
|
7973
|
+
}
|
|
7974
|
+
async function metaPostPageFeed(opts) {
|
|
7975
|
+
const { pageId, pageAccessToken, message } = opts;
|
|
7976
|
+
const pid = String(pageId || "").trim();
|
|
7977
|
+
const tok = String(pageAccessToken || "").trim();
|
|
7978
|
+
if (!pid || !tok) throw new Error("Page ID and page access token are required");
|
|
7979
|
+
const url = new URL(`${GRAPH_BASE}/${encodeURIComponent(pid)}/feed`);
|
|
7980
|
+
const params = new URLSearchParams();
|
|
7981
|
+
params.set("access_token", tok);
|
|
7982
|
+
params.set("message", String(message || "").trim().slice(0, 5e3));
|
|
7983
|
+
const r = await fetch(url.toString(), {
|
|
7984
|
+
method: "POST",
|
|
7985
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
7986
|
+
body: params.toString()
|
|
7987
|
+
});
|
|
7988
|
+
const text = await r.text();
|
|
7989
|
+
return parseGraphMutation(text, r.ok);
|
|
7990
|
+
}
|
|
7991
|
+
|
|
7992
|
+
// src/plugins/social-media/social-media-api-handlers.ts
|
|
7993
|
+
var SETTINGS_GROUP = "social_media";
|
|
7994
|
+
async function loadGroupMap(dataSource, entityMap, encryptionKey) {
|
|
7995
|
+
if (!entityMap.configs) return {};
|
|
7996
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
7997
|
+
const rows = await repo.find({ where: { settings: SETTINGS_GROUP, deleted: false } });
|
|
7998
|
+
const out = {};
|
|
7999
|
+
for (const row of rows) {
|
|
8000
|
+
let val = row.value;
|
|
8001
|
+
if (row.encrypted && encryptionKey) {
|
|
8002
|
+
try {
|
|
8003
|
+
val = simpleDecrypt(val, encryptionKey);
|
|
8004
|
+
} catch {
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
out[row.key] = val;
|
|
8008
|
+
}
|
|
8009
|
+
return out;
|
|
8010
|
+
}
|
|
8011
|
+
async function upsertConfigKey(dataSource, entityMap, key, value, opts) {
|
|
8012
|
+
if (!entityMap.configs) throw new Error("configs entity missing");
|
|
8013
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
8014
|
+
let stored = value;
|
|
8015
|
+
if (opts.encrypted && opts.encryptionKey) {
|
|
8016
|
+
stored = simpleEncrypt(value, opts.encryptionKey);
|
|
8017
|
+
}
|
|
8018
|
+
const existing = await repo.findOne({ where: { settings: SETTINGS_GROUP, key } });
|
|
8019
|
+
if (existing) {
|
|
8020
|
+
await repo.update(existing.id, {
|
|
8021
|
+
value: stored,
|
|
8022
|
+
type: opts.type,
|
|
8023
|
+
encrypted: opts.encrypted,
|
|
8024
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
8025
|
+
});
|
|
8026
|
+
} else {
|
|
8027
|
+
await repo.save(
|
|
8028
|
+
repo.create({
|
|
8029
|
+
settings: SETTINGS_GROUP,
|
|
8030
|
+
key,
|
|
8031
|
+
value: stored,
|
|
8032
|
+
type: opts.type,
|
|
8033
|
+
encrypted: opts.encrypted
|
|
8034
|
+
})
|
|
8035
|
+
);
|
|
8036
|
+
}
|
|
8037
|
+
}
|
|
8038
|
+
function stripHtml(html, maxLen) {
|
|
8039
|
+
const t = String(html || "").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8040
|
+
return t.slice(0, maxLen);
|
|
8041
|
+
}
|
|
8042
|
+
function decodeHtmlAttr(s) {
|
|
8043
|
+
return String(s).replace(/&/gi, "&").replace(/"/gi, '"').replace(/'/g, "'").replace(/</gi, "<").replace(/>/gi, ">");
|
|
8044
|
+
}
|
|
8045
|
+
function extractFirstImageSrcFromHtml(html) {
|
|
8046
|
+
const s = String(html);
|
|
8047
|
+
const patterns = [
|
|
8048
|
+
/<img[^>]*\bsrc\s*=\s*["']([^"']+)["']/i,
|
|
8049
|
+
/<img[^>]*\bsrc\s*=\s*([^\s>]+)/i,
|
|
8050
|
+
/<img[^>]*\bdata-src\s*=\s*["']([^"']+)["']/i,
|
|
8051
|
+
/<img[^>]*\bdata-src\s*=\s*([^\s>]+)/i
|
|
8052
|
+
];
|
|
8053
|
+
for (const re of patterns) {
|
|
8054
|
+
const m = s.match(re);
|
|
8055
|
+
const raw = m?.[1]?.trim();
|
|
8056
|
+
if (raw) return decodeHtmlAttr(raw);
|
|
8057
|
+
}
|
|
8058
|
+
return null;
|
|
8059
|
+
}
|
|
8060
|
+
function resolveAbsoluteUrl(publicSiteUrl, pathOrUrl) {
|
|
8061
|
+
if (!pathOrUrl || !String(pathOrUrl).trim()) return null;
|
|
8062
|
+
const s = decodeHtmlAttr(String(pathOrUrl).trim());
|
|
8063
|
+
if (/^https?:\/\//i.test(s)) return s;
|
|
8064
|
+
if (s.startsWith("//")) return `https:${s}`;
|
|
8065
|
+
const base = (publicSiteUrl || "").replace(/\/+$/, "");
|
|
8066
|
+
if (!base) return null;
|
|
8067
|
+
const rel = s.startsWith("/") ? s : `/${s}`;
|
|
8068
|
+
return `${base}${rel}`;
|
|
8069
|
+
}
|
|
8070
|
+
function guessImageContentType(filePathOrUrl, buf) {
|
|
8071
|
+
const lower = filePathOrUrl.toLowerCase();
|
|
8072
|
+
if (lower.includes(".png")) return "image/png";
|
|
8073
|
+
if (lower.includes(".webp")) return "image/webp";
|
|
8074
|
+
if (lower.includes(".gif")) return "image/gif";
|
|
8075
|
+
if (lower.includes(".jpg") || lower.includes(".jpeg")) return "image/jpeg";
|
|
8076
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 216) return "image/jpeg";
|
|
8077
|
+
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
8078
|
+
if (buf.length >= 12 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return "image/webp";
|
|
8079
|
+
return "image/jpeg";
|
|
8080
|
+
}
|
|
8081
|
+
function isLinkedInFeedshareCompatibleContentType(mime) {
|
|
8082
|
+
const m = mime.split(";")[0].trim().toLowerCase();
|
|
8083
|
+
return m === "image/jpeg" || m === "image/png";
|
|
8084
|
+
}
|
|
8085
|
+
async function loadImageBytesForBlogShare(coverImage, contentHtml, publicSiteUrl, req) {
|
|
8086
|
+
const origin = publicSiteUrl || inferRequestOrigin(req) || "";
|
|
8087
|
+
const fromHtml = extractFirstImageSrcFromHtml(contentHtml);
|
|
8088
|
+
const candidates = [];
|
|
8089
|
+
const seenDecoded = /* @__PURE__ */ new Set();
|
|
8090
|
+
const pushCand = (role, raw) => {
|
|
8091
|
+
const decoded = decodeHtmlAttr(raw.trim());
|
|
8092
|
+
if (!decoded || decoded.startsWith("data:")) return;
|
|
8093
|
+
if (seenDecoded.has(decoded)) return;
|
|
8094
|
+
seenDecoded.add(decoded);
|
|
8095
|
+
candidates.push({ role, raw });
|
|
8096
|
+
};
|
|
8097
|
+
if (coverImage?.trim()) pushCand("cover", coverImage.trim());
|
|
8098
|
+
if (fromHtml) pushCand("bodyImage", fromHtml);
|
|
8099
|
+
for (const { role, raw } of candidates) {
|
|
8100
|
+
const decoded = decodeHtmlAttr(raw.trim());
|
|
8101
|
+
if (decoded.startsWith("data:")) continue;
|
|
8102
|
+
if (decoded.startsWith("/uploads/") || decoded.startsWith("uploads/")) {
|
|
8103
|
+
const rel = decoded.startsWith("/") ? decoded.slice(1) : decoded;
|
|
8104
|
+
const tries = [import_node_path.default.join(process.cwd(), "public", rel), import_node_path.default.join(process.cwd(), rel)];
|
|
8105
|
+
for (const filePath of tries) {
|
|
8106
|
+
try {
|
|
8107
|
+
const buf = await import_promises.default.readFile(filePath);
|
|
8108
|
+
if (buf.length > 0) {
|
|
8109
|
+
const contentType = guessImageContentType(filePath, buf);
|
|
8110
|
+
if (!isLinkedInFeedshareCompatibleContentType(contentType)) {
|
|
8111
|
+
continue;
|
|
8112
|
+
}
|
|
8113
|
+
return {
|
|
8114
|
+
buffer: buf,
|
|
8115
|
+
contentType,
|
|
8116
|
+
source: `${role}:local_file:${truncateForLog(rel, 120)}`
|
|
8117
|
+
};
|
|
8118
|
+
}
|
|
8119
|
+
} catch {
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
}
|
|
8123
|
+
const url = resolveAbsoluteUrl(origin, decoded);
|
|
8124
|
+
if (!url) continue;
|
|
8125
|
+
try {
|
|
8126
|
+
const imgRes = await fetch(url, {
|
|
8127
|
+
headers: {
|
|
8128
|
+
"User-Agent": "InfuroCMS/1.0 (social share; +https://infuro.com)",
|
|
8129
|
+
Accept: "image/*,*/*;q=0.5"
|
|
8130
|
+
}
|
|
8131
|
+
});
|
|
8132
|
+
if (!imgRes.ok) continue;
|
|
8133
|
+
const buf = Buffer.from(await imgRes.arrayBuffer());
|
|
8134
|
+
if (buf.length === 0) continue;
|
|
8135
|
+
const hdr = imgRes.headers.get("content-type");
|
|
8136
|
+
const contentType = hdr && /^image\//i.test(hdr) ? hdr.split(";")[0].trim() : guessImageContentType(url, buf);
|
|
8137
|
+
if (!isLinkedInFeedshareCompatibleContentType(contentType)) {
|
|
8138
|
+
continue;
|
|
8139
|
+
}
|
|
8140
|
+
return {
|
|
8141
|
+
buffer: buf,
|
|
8142
|
+
contentType,
|
|
8143
|
+
source: `${role}:fetch:${truncateForLog(url, 160)}`
|
|
8144
|
+
};
|
|
8145
|
+
} catch {
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
return null;
|
|
8149
|
+
}
|
|
8150
|
+
function inferRequestOrigin(req) {
|
|
8151
|
+
const host = req.headers.get("x-forwarded-host") || req.headers.get("host");
|
|
8152
|
+
if (!host) return void 0;
|
|
8153
|
+
const rawProto = req.headers.get("x-forwarded-proto") || "https";
|
|
8154
|
+
const proto = rawProto.split(",")[0]?.trim() || "https";
|
|
8155
|
+
return `${proto}://${host}`.replace(/\/+$/, "");
|
|
8156
|
+
}
|
|
8157
|
+
function linkedInPublishLoggingEnabled(config) {
|
|
8158
|
+
if (config.linkedInPublishLogging === false) return false;
|
|
8159
|
+
if (config.linkedInPublishLogging === true) return true;
|
|
8160
|
+
try {
|
|
8161
|
+
if (typeof process !== "undefined" && String(process.env.CMS_LINKEDIN_PUBLISH_LOG).trim() === "0") {
|
|
8162
|
+
return false;
|
|
8163
|
+
}
|
|
8164
|
+
} catch {
|
|
8165
|
+
}
|
|
8166
|
+
return true;
|
|
8167
|
+
}
|
|
8168
|
+
function logLinkedInPublish(enabled, event, detail) {
|
|
8169
|
+
if (!enabled) return;
|
|
8170
|
+
const line = `[LinkedIn publish] ${event} ${JSON.stringify(detail)}`;
|
|
8171
|
+
if (event === "failed" || event === "image_path_failed_falling_back_to_text") {
|
|
8172
|
+
console.warn(line);
|
|
8173
|
+
} else {
|
|
8174
|
+
console.info(line);
|
|
8175
|
+
}
|
|
8176
|
+
}
|
|
8177
|
+
function truncateForLog(s, max = 160) {
|
|
8178
|
+
const t = String(s).replace(/\s+/g, " ").trim();
|
|
8179
|
+
if (t.length <= max) return t;
|
|
8180
|
+
return `${t.slice(0, max)}\u2026`;
|
|
8181
|
+
}
|
|
8182
|
+
function createSocialMediaHandlers(config) {
|
|
8183
|
+
const {
|
|
8184
|
+
dataSource,
|
|
8185
|
+
entityMap,
|
|
8186
|
+
json,
|
|
8187
|
+
requireAuth,
|
|
8188
|
+
requireEntityPermission,
|
|
8189
|
+
encryptionKey,
|
|
8190
|
+
publicSiteUrl
|
|
8191
|
+
} = config;
|
|
8192
|
+
const publishLog = linkedInPublishLoggingEnabled(config);
|
|
8193
|
+
return {
|
|
8194
|
+
async getLinkedInStatus(req) {
|
|
8195
|
+
const a = await requireAuth(req);
|
|
8196
|
+
if (a) return a;
|
|
8197
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8198
|
+
if (pe) return pe;
|
|
8199
|
+
try {
|
|
8200
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8201
|
+
const enabled = map.enabled !== "false";
|
|
8202
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8203
|
+
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
8204
|
+
return json({
|
|
8205
|
+
ok: true,
|
|
8206
|
+
linkedInReady: enabled && Boolean(token) && Boolean(sub),
|
|
8207
|
+
enabled,
|
|
8208
|
+
hasToken: Boolean(token),
|
|
8209
|
+
hasPersonSub: Boolean(sub)
|
|
8210
|
+
});
|
|
8211
|
+
} catch (e) {
|
|
8212
|
+
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8213
|
+
return json({ error: msg }, { status: 500 });
|
|
8214
|
+
}
|
|
8215
|
+
},
|
|
8216
|
+
async syncLinkedInProfile(req) {
|
|
8217
|
+
const a = await requireAuth(req);
|
|
8218
|
+
if (a) return a;
|
|
8219
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8220
|
+
if (pe) return pe;
|
|
8221
|
+
try {
|
|
8222
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8223
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8224
|
+
if (!token) {
|
|
8225
|
+
return json({ error: "Save a LinkedIn access token in Plugins \u2192 Social media first." }, { status: 400 });
|
|
8226
|
+
}
|
|
8227
|
+
const info = await linkedInGetUserinfo(token);
|
|
8228
|
+
const sub = String(info.sub ?? "").trim();
|
|
8229
|
+
if (!sub) {
|
|
8230
|
+
return json({ error: "LinkedIn userinfo did not return a subject (sub)." }, { status: 502 });
|
|
8231
|
+
}
|
|
8232
|
+
await upsertConfigKey(dataSource, entityMap, "linkedin_person_sub", sub, {
|
|
8233
|
+
type: "private",
|
|
8234
|
+
encrypted: false,
|
|
8235
|
+
encryptionKey
|
|
8236
|
+
});
|
|
8237
|
+
return json({
|
|
8238
|
+
ok: true,
|
|
8239
|
+
linkedin_person_sub: sub,
|
|
8240
|
+
name: info.name ?? null
|
|
8241
|
+
});
|
|
8242
|
+
} catch (e) {
|
|
8243
|
+
const msg = e instanceof Error ? e.message : "Sync failed";
|
|
8244
|
+
return json({ error: msg }, { status: 502 });
|
|
8245
|
+
}
|
|
8246
|
+
},
|
|
8247
|
+
async publishBlogToLinkedIn(req, blogId) {
|
|
8248
|
+
const a = await requireAuth(req);
|
|
8249
|
+
if (a) return a;
|
|
8250
|
+
const pe = await requireEntityPermission(req, "blogs", "update");
|
|
8251
|
+
if (pe) return pe;
|
|
8252
|
+
if (!Number.isFinite(blogId) || blogId < 1) {
|
|
8253
|
+
return json({ error: "Invalid blog id" }, { status: 400 });
|
|
8254
|
+
}
|
|
8255
|
+
if (!entityMap.blogs) {
|
|
8256
|
+
return json({ error: "Blogs entity not configured" }, { status: 503 });
|
|
8257
|
+
}
|
|
8258
|
+
try {
|
|
8259
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8260
|
+
if (map.enabled === "false") {
|
|
8261
|
+
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8262
|
+
}
|
|
8263
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8264
|
+
const personSub = (map.linkedin_person_sub ?? "").trim();
|
|
8265
|
+
if (!token || !personSub) {
|
|
8266
|
+
return json(
|
|
8267
|
+
{ error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
|
|
8268
|
+
{ status: 400 }
|
|
8269
|
+
);
|
|
8270
|
+
}
|
|
8271
|
+
const blogRepo = dataSource.getRepository(entityMap.blogs);
|
|
8272
|
+
const blog = await blogRepo.findOne({
|
|
8273
|
+
where: { id: blogId, deleted: false }
|
|
8274
|
+
});
|
|
8275
|
+
if (!blog) {
|
|
8276
|
+
return json({ error: "Blog not found" }, { status: 404 });
|
|
8277
|
+
}
|
|
8278
|
+
const b = blog;
|
|
8279
|
+
const title = String(b.title ?? "Blog").trim() || "Blog";
|
|
8280
|
+
const social = String(b.socialMediaContent ?? "").trim();
|
|
8281
|
+
const excerpt = stripHtml(String(b.content ?? ""), 2800);
|
|
8282
|
+
const commentary = (social || `${title}
|
|
8283
|
+
|
|
8284
|
+
${excerpt}`).trim().slice(0, 2900);
|
|
8285
|
+
const commentaryFrom = social ? "socialMediaContent" : "titleAndExcerpt";
|
|
8286
|
+
logLinkedInPublish(publishLog, "start", {
|
|
8287
|
+
blogId,
|
|
8288
|
+
titleLen: title.length,
|
|
8289
|
+
commentaryFrom,
|
|
8290
|
+
commentaryChars: commentary.length,
|
|
8291
|
+
hasCover: Boolean(b.coverImage?.trim()),
|
|
8292
|
+
contentHtmlChars: String(b.content ?? "").length
|
|
8293
|
+
});
|
|
8294
|
+
const imagePayload = await loadImageBytesForBlogShare(
|
|
8295
|
+
b.coverImage,
|
|
8296
|
+
String(b.content ?? ""),
|
|
8297
|
+
publicSiteUrl || inferRequestOrigin(req),
|
|
8298
|
+
req
|
|
8299
|
+
);
|
|
8300
|
+
if (imagePayload) {
|
|
8301
|
+
logLinkedInPublish(publishLog, "image_bytes_ready", {
|
|
8302
|
+
blogId,
|
|
8303
|
+
source: imagePayload.source,
|
|
8304
|
+
bytes: imagePayload.buffer.length,
|
|
8305
|
+
contentType: imagePayload.contentType
|
|
8306
|
+
});
|
|
8307
|
+
try {
|
|
8308
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, personSub);
|
|
8309
|
+
logLinkedInPublish(publishLog, "image_registered", {
|
|
8310
|
+
blogId,
|
|
8311
|
+
asset: truncateForLog(asset, 120),
|
|
8312
|
+
uploadHost: (() => {
|
|
8313
|
+
try {
|
|
8314
|
+
return new URL(uploadUrl).host;
|
|
8315
|
+
} catch {
|
|
8316
|
+
return "unknown";
|
|
8317
|
+
}
|
|
8318
|
+
})()
|
|
8319
|
+
});
|
|
8320
|
+
await linkedInUploadBinary(token, uploadUrl, imagePayload.buffer, imagePayload.contentType);
|
|
8321
|
+
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
8322
|
+
const out2 = await linkedInCreateImageShare({
|
|
8323
|
+
accessToken: token,
|
|
8324
|
+
personSub,
|
|
8325
|
+
asset,
|
|
8326
|
+
commentary,
|
|
8327
|
+
title,
|
|
8328
|
+
description: title
|
|
8329
|
+
});
|
|
8330
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8331
|
+
blogId,
|
|
8332
|
+
mode: "image_plus_text",
|
|
8333
|
+
linkedinPostId: out2.id ?? null,
|
|
8334
|
+
commentaryFrom,
|
|
8335
|
+
commentaryChars: commentary.length,
|
|
8336
|
+
imageSource: imagePayload.source
|
|
8337
|
+
});
|
|
8338
|
+
return json({
|
|
8339
|
+
ok: true,
|
|
8340
|
+
linkedin: out2,
|
|
8341
|
+
usedImage: true,
|
|
8342
|
+
publishSummary: {
|
|
8343
|
+
mode: "image_plus_text",
|
|
8344
|
+
commentaryFrom,
|
|
8345
|
+
commentaryChars: commentary.length,
|
|
8346
|
+
image: {
|
|
8347
|
+
bytes: imagePayload.buffer.length,
|
|
8348
|
+
contentType: imagePayload.contentType,
|
|
8349
|
+
source: imagePayload.source
|
|
8350
|
+
},
|
|
8351
|
+
linkedinPostId: out2.id ?? null
|
|
8352
|
+
}
|
|
8353
|
+
});
|
|
8354
|
+
} catch (imgErr) {
|
|
8355
|
+
const imgMsg = imgErr instanceof Error ? imgErr.message : String(imgErr);
|
|
8356
|
+
logLinkedInPublish(publishLog, "image_path_failed_falling_back_to_text", {
|
|
8357
|
+
blogId,
|
|
8358
|
+
message: truncateForLog(imgMsg, 500)
|
|
8359
|
+
});
|
|
8360
|
+
const out2 = await linkedInCreateTextShare({
|
|
8361
|
+
accessToken: token,
|
|
8362
|
+
personSub,
|
|
8363
|
+
commentary
|
|
8364
|
+
});
|
|
8365
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8366
|
+
blogId,
|
|
8367
|
+
mode: "text_only_after_image_failure",
|
|
8368
|
+
linkedinPostId: out2.id ?? null,
|
|
8369
|
+
commentaryFrom,
|
|
8370
|
+
commentaryChars: commentary.length,
|
|
8371
|
+
imageError: truncateForLog(imgMsg, 400)
|
|
8372
|
+
});
|
|
8373
|
+
return json({
|
|
8374
|
+
ok: true,
|
|
8375
|
+
linkedin: out2,
|
|
8376
|
+
usedImage: false,
|
|
8377
|
+
usedTextOnly: true,
|
|
8378
|
+
imagePathError: imgMsg,
|
|
8379
|
+
publishSummary: {
|
|
8380
|
+
mode: "text_only_after_image_failure",
|
|
8381
|
+
commentaryFrom,
|
|
8382
|
+
commentaryChars: commentary.length,
|
|
8383
|
+
linkedinPostId: out2.id ?? null,
|
|
8384
|
+
imageAttempt: {
|
|
8385
|
+
bytes: imagePayload.buffer.length,
|
|
8386
|
+
contentType: imagePayload.contentType,
|
|
8387
|
+
source: imagePayload.source
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
});
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
logLinkedInPublish(publishLog, "no_image_bytes", {
|
|
8394
|
+
blogId,
|
|
8395
|
+
commentaryFrom,
|
|
8396
|
+
commentaryChars: commentary.length,
|
|
8397
|
+
note: "posting_text_only"
|
|
8398
|
+
});
|
|
8399
|
+
const out = await linkedInCreateTextShare({
|
|
8400
|
+
accessToken: token,
|
|
8401
|
+
personSub,
|
|
8402
|
+
commentary
|
|
8403
|
+
});
|
|
8404
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8405
|
+
blogId,
|
|
8406
|
+
mode: "text_only",
|
|
8407
|
+
linkedinPostId: out.id ?? null,
|
|
8408
|
+
commentaryFrom,
|
|
8409
|
+
commentaryChars: commentary.length
|
|
8410
|
+
});
|
|
8411
|
+
return json({
|
|
8412
|
+
ok: true,
|
|
8413
|
+
linkedin: out,
|
|
8414
|
+
usedTextOnly: true,
|
|
8415
|
+
publishSummary: {
|
|
8416
|
+
mode: "text_only",
|
|
8417
|
+
commentaryFrom,
|
|
8418
|
+
commentaryChars: commentary.length,
|
|
8419
|
+
linkedinPostId: out.id ?? null
|
|
8420
|
+
},
|
|
8421
|
+
hint: "No image bytes loaded, or only non-JPEG/PNG images were found. Use JPEG or PNG for cover or an inline <img>, set CMS_PUBLIC_URL for absolute URLs, or paths under /uploads/. LinkedIn feedshare uploads expect JPEG or PNG."
|
|
8422
|
+
});
|
|
8423
|
+
} catch (e) {
|
|
8424
|
+
const msg = e instanceof Error ? e.message : "Publish failed";
|
|
8425
|
+
logLinkedInPublish(publishLog, "failed", { blogId, message: truncateForLog(msg, 600) });
|
|
8426
|
+
return json({ error: msg }, { status: 502 });
|
|
8427
|
+
}
|
|
8428
|
+
},
|
|
8429
|
+
async getFacebookStatus(req) {
|
|
8430
|
+
const a = await requireAuth(req);
|
|
8431
|
+
if (a) return a;
|
|
8432
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8433
|
+
if (pe) return pe;
|
|
8434
|
+
try {
|
|
8435
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8436
|
+
const enabled = map.enabled !== "false";
|
|
8437
|
+
const userToken = (map.meta_user_access_token ?? "").trim();
|
|
8438
|
+
const pageId = (map.meta_facebook_page_id ?? "").trim();
|
|
8439
|
+
let hasPageToken = false;
|
|
8440
|
+
if (enabled && userToken && pageId) {
|
|
8441
|
+
try {
|
|
8442
|
+
const accounts = await metaFetchUserManagedPages(userToken);
|
|
8443
|
+
hasPageToken = Boolean(metaResolvePageAccessToken(accounts, pageId));
|
|
8444
|
+
} catch {
|
|
8445
|
+
hasPageToken = false;
|
|
8446
|
+
}
|
|
8447
|
+
}
|
|
8448
|
+
const facebookReady = enabled && Boolean(userToken) && Boolean(pageId);
|
|
8449
|
+
return json({
|
|
8450
|
+
ok: true,
|
|
8451
|
+
facebookReady,
|
|
8452
|
+
enabled,
|
|
8453
|
+
hasUserToken: Boolean(userToken),
|
|
8454
|
+
hasPageId: Boolean(pageId),
|
|
8455
|
+
hasPageToken
|
|
8456
|
+
});
|
|
8457
|
+
} catch (e) {
|
|
8458
|
+
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8459
|
+
return json({ error: msg }, { status: 500 });
|
|
8460
|
+
}
|
|
8461
|
+
},
|
|
8462
|
+
async publishBlogToFacebook(req, blogId) {
|
|
8463
|
+
const a = await requireAuth(req);
|
|
8464
|
+
if (a) return a;
|
|
8465
|
+
const pe = await requireEntityPermission(req, "blogs", "update");
|
|
8466
|
+
if (pe) return pe;
|
|
8467
|
+
if (!Number.isFinite(blogId) || blogId < 1) {
|
|
8468
|
+
return json({ error: "Invalid blog id" }, { status: 400 });
|
|
8469
|
+
}
|
|
8470
|
+
if (!entityMap.blogs) {
|
|
8471
|
+
return json({ error: "Blogs entity not configured" }, { status: 503 });
|
|
8472
|
+
}
|
|
8473
|
+
try {
|
|
8474
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8475
|
+
if (map.enabled === "false") {
|
|
8476
|
+
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8477
|
+
}
|
|
8478
|
+
const userToken = (map.meta_user_access_token ?? "").trim();
|
|
8479
|
+
const pageId = (map.meta_facebook_page_id ?? "").trim();
|
|
8480
|
+
if (!userToken || !pageId) {
|
|
8481
|
+
return json(
|
|
8482
|
+
{
|
|
8483
|
+
error: "Configure Meta user access token and Facebook Page ID in Plugins \u2192 Social media \u2192 Facebook, then save."
|
|
8484
|
+
},
|
|
8485
|
+
{ status: 400 }
|
|
8486
|
+
);
|
|
8487
|
+
}
|
|
8488
|
+
let accounts;
|
|
8489
|
+
try {
|
|
8490
|
+
accounts = await metaFetchUserManagedPages(userToken);
|
|
8491
|
+
} catch (e) {
|
|
8492
|
+
const msg = e instanceof Error ? e.message : "Failed to load Facebook pages";
|
|
8493
|
+
return json({ error: msg }, { status: 502 });
|
|
8494
|
+
}
|
|
8495
|
+
const pageAccessToken = metaResolvePageAccessToken(accounts, pageId);
|
|
8496
|
+
if (!pageAccessToken) {
|
|
8497
|
+
return json(
|
|
8498
|
+
{
|
|
8499
|
+
error: "No page access token for the saved Page ID. In Plugins \u2192 Facebook, use Fetch managed pages, copy a Page `id` you manage, set Target Page ID, and save."
|
|
8500
|
+
},
|
|
8501
|
+
{ status: 400 }
|
|
8502
|
+
);
|
|
8503
|
+
}
|
|
8504
|
+
const blogRepo = dataSource.getRepository(entityMap.blogs);
|
|
8505
|
+
const blog = await blogRepo.findOne({
|
|
8506
|
+
where: { id: blogId, deleted: false }
|
|
8507
|
+
});
|
|
8508
|
+
if (!blog) {
|
|
8509
|
+
return json({ error: "Blog not found" }, { status: 404 });
|
|
8510
|
+
}
|
|
8511
|
+
const b = blog;
|
|
8512
|
+
const title = String(b.title ?? "Blog").trim() || "Blog";
|
|
8513
|
+
const social = String(b.socialMediaContent ?? "").trim();
|
|
8514
|
+
const excerpt = stripHtml(String(b.content ?? ""), 2800);
|
|
8515
|
+
const caption = (social || `${title}
|
|
8516
|
+
|
|
8517
|
+
${excerpt}`).trim().slice(0, 2200);
|
|
8518
|
+
const imagePayload = await loadImageBytesForBlogShare(
|
|
8519
|
+
b.coverImage,
|
|
8520
|
+
String(b.content ?? ""),
|
|
8521
|
+
publicSiteUrl || inferRequestOrigin(req),
|
|
8522
|
+
req
|
|
8523
|
+
);
|
|
8524
|
+
if (imagePayload) {
|
|
8525
|
+
const out2 = await metaPostPagePhoto({
|
|
8526
|
+
pageId,
|
|
8527
|
+
pageAccessToken,
|
|
8528
|
+
imageBuffer: imagePayload.buffer,
|
|
8529
|
+
contentType: imagePayload.contentType,
|
|
8530
|
+
caption
|
|
8531
|
+
});
|
|
8532
|
+
return json({
|
|
8533
|
+
ok: true,
|
|
8534
|
+
facebook: out2,
|
|
8535
|
+
usedImage: true,
|
|
8536
|
+
publishSummary: {
|
|
8537
|
+
mode: "page_photo",
|
|
8538
|
+
captionChars: caption.length,
|
|
8539
|
+
imageSource: imagePayload.source,
|
|
8540
|
+
facebookPostId: out2.post_id ?? out2.id ?? null
|
|
8541
|
+
}
|
|
8542
|
+
});
|
|
8543
|
+
}
|
|
8544
|
+
const out = await metaPostPageFeed({
|
|
8545
|
+
pageId,
|
|
8546
|
+
pageAccessToken,
|
|
8547
|
+
message: caption
|
|
8548
|
+
});
|
|
8549
|
+
return json({
|
|
8550
|
+
ok: true,
|
|
8551
|
+
facebook: out,
|
|
8552
|
+
usedImage: false,
|
|
8553
|
+
usedFeed: true,
|
|
8554
|
+
publishSummary: {
|
|
8555
|
+
mode: "page_feed_text",
|
|
8556
|
+
messageChars: caption.length,
|
|
8557
|
+
facebookPostId: out.id ?? null
|
|
8558
|
+
},
|
|
8559
|
+
hint: "Posted as a Page text update (no JPEG/PNG cover or body image found). Add a JPEG/PNG cover or inline image for a photo post."
|
|
8560
|
+
});
|
|
8561
|
+
} catch (e) {
|
|
8562
|
+
const msg = e instanceof Error ? e.message : "Facebook publish failed";
|
|
8563
|
+
return json({ error: msg }, { status: 502 });
|
|
8564
|
+
}
|
|
8565
|
+
},
|
|
8566
|
+
/**
|
|
8567
|
+
* POST JSON `{ appId, appSecret, userAccessToken }`. Calls Graph `GET /me/accounts` with the user token.
|
|
8568
|
+
* App ID and secret are validated but not used yet (reserved for future token flows).
|
|
8569
|
+
* Persist credentials via Plugins → Save: `meta_app_id` (public), `meta_app_secret` and `meta_user_access_token` (encrypted).
|
|
8570
|
+
*/
|
|
8571
|
+
async fetchFacebookManagedPages(req) {
|
|
8572
|
+
const a = await requireAuth(req);
|
|
8573
|
+
if (a) return a;
|
|
8574
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
8575
|
+
if (pe) return pe;
|
|
8576
|
+
let body;
|
|
8577
|
+
try {
|
|
8578
|
+
body = await req.json();
|
|
8579
|
+
} catch {
|
|
8580
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8581
|
+
}
|
|
8582
|
+
const appId = String(body?.appId ?? "").trim();
|
|
8583
|
+
const appSecret = String(body?.appSecret ?? "").trim();
|
|
8584
|
+
const userAccessToken = String(body?.userAccessToken ?? "").trim();
|
|
8585
|
+
if (!appId || !appSecret || !userAccessToken) {
|
|
8586
|
+
return json(
|
|
8587
|
+
{ error: "App ID, app secret, and user access token are required." },
|
|
8588
|
+
{ status: 400 }
|
|
8589
|
+
);
|
|
8590
|
+
}
|
|
8591
|
+
void appId;
|
|
8592
|
+
void appSecret;
|
|
8593
|
+
try {
|
|
8594
|
+
const graph = await metaFetchUserManagedPages(userAccessToken);
|
|
8595
|
+
return json({ ok: true, graph });
|
|
8596
|
+
} catch (e) {
|
|
8597
|
+
const msg = e instanceof Error ? e.message : "Facebook Graph request failed";
|
|
8598
|
+
return json({ error: msg }, { status: 502 });
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
};
|
|
8602
|
+
}
|
|
8603
|
+
|
|
7272
8604
|
// src/api/cms-api-handler.ts
|
|
7273
8605
|
var KNOWLEDGE_SUFFIX = "knowledge";
|
|
7274
8606
|
var CMS_API_LOG = "[cms-api]";
|
|
@@ -7277,7 +8609,9 @@ function withLlmKnowledgeEntityFallbacks(base) {
|
|
|
7277
8609
|
"llm_agents",
|
|
7278
8610
|
"llm_agent_knowledge_documents",
|
|
7279
8611
|
"knowledge_base_documents",
|
|
7280
|
-
"knowledge_base_chunks"
|
|
8612
|
+
"knowledge_base_chunks",
|
|
8613
|
+
"rss_feeds",
|
|
8614
|
+
"rss_articles"
|
|
7281
8615
|
];
|
|
7282
8616
|
const patch = {};
|
|
7283
8617
|
for (const key of keys) {
|
|
@@ -7293,8 +8627,8 @@ function withLlmKnowledgeEntityFallbacks(base) {
|
|
|
7293
8627
|
}
|
|
7294
8628
|
return merged;
|
|
7295
8629
|
}
|
|
7296
|
-
function matchLlmAgentKnowledgeRoute(
|
|
7297
|
-
const p =
|
|
8630
|
+
function matchLlmAgentKnowledgeRoute(path2) {
|
|
8631
|
+
const p = path2[0] === "api" ? path2.slice(1) : path2;
|
|
7298
8632
|
if (p[0] !== "llm_agents" || p.length < 2) return null;
|
|
7299
8633
|
const seg1 = p[1];
|
|
7300
8634
|
if (!seg1) return null;
|
|
@@ -7325,7 +8659,9 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
7325
8659
|
"wishlists",
|
|
7326
8660
|
"wishlist_items",
|
|
7327
8661
|
"message_templates",
|
|
7328
|
-
"llm_agent_knowledge_documents"
|
|
8662
|
+
"llm_agent_knowledge_documents",
|
|
8663
|
+
"rss_feeds",
|
|
8664
|
+
"rss_articles"
|
|
7329
8665
|
]);
|
|
7330
8666
|
function createCmsApiHandler(config) {
|
|
7331
8667
|
const {
|
|
@@ -7452,6 +8788,23 @@ function createCmsApiHandler(config) {
|
|
|
7452
8788
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
7453
8789
|
const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
7454
8790
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
8791
|
+
function resolvePublicSiteUrlForSocial() {
|
|
8792
|
+
if (typeof process === "undefined") return void 0;
|
|
8793
|
+
const u = (process.env.CMS_PUBLIC_URL || process.env.NEXT_PUBLIC_SITE_URL || "").trim();
|
|
8794
|
+
if (u) return u.replace(/\/+$/, "");
|
|
8795
|
+
const v = (process.env.VERCEL_URL || "").trim();
|
|
8796
|
+
if (!v) return void 0;
|
|
8797
|
+
return v.startsWith("http") ? v.replace(/\/+$/, "") : `https://${v.replace(/\/+$/, "")}`;
|
|
8798
|
+
}
|
|
8799
|
+
const socialMediaHandlers = settingsConfig?.dataSource && entityMap.configs ? createSocialMediaHandlers({
|
|
8800
|
+
dataSource: settingsConfig.dataSource,
|
|
8801
|
+
entityMap,
|
|
8802
|
+
json: config.json,
|
|
8803
|
+
requireAuth: config.requireAuth,
|
|
8804
|
+
requireEntityPermission: requireEntityPermissionEffective,
|
|
8805
|
+
encryptionKey: settingsConfig.encryptionKey,
|
|
8806
|
+
publicSiteUrl: resolvePublicSiteUrlForSocial()
|
|
8807
|
+
}) : null;
|
|
7455
8808
|
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
7456
8809
|
dataSource,
|
|
7457
8810
|
entityMap,
|
|
@@ -7478,95 +8831,526 @@ function createCmsApiHandler(config) {
|
|
|
7478
8831
|
return {
|
|
7479
8832
|
async handle(method, pathInput, req) {
|
|
7480
8833
|
const m = typeof method === "string" ? method.toUpperCase() : "GET";
|
|
7481
|
-
const
|
|
8834
|
+
const path2 = pathInput.length > 0 && pathInput[0] === "api" ? pathInput.slice(1) : pathInput;
|
|
7482
8835
|
async function analyticsGate() {
|
|
7483
8836
|
const a = await config.requireAuth(req);
|
|
7484
8837
|
if (a) return a;
|
|
7485
8838
|
return requireEntityPermissionEffective(req, "analytics", "read");
|
|
7486
8839
|
}
|
|
7487
|
-
if (
|
|
8840
|
+
if (path2[0] === "admin" && path2[1] === "roles") {
|
|
7488
8841
|
if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
|
|
7489
|
-
if (
|
|
7490
|
-
if (
|
|
7491
|
-
if (
|
|
7492
|
-
if (
|
|
7493
|
-
if (
|
|
8842
|
+
if (path2.length === 2 && m === "GET") return adminRoles.list();
|
|
8843
|
+
if (path2.length === 2 && m === "POST") return adminRoles.createGroup(req);
|
|
8844
|
+
if (path2.length === 3 && m === "PATCH") return adminRoles.patchGroup(req, path2[2]);
|
|
8845
|
+
if (path2.length === 3 && m === "DELETE") return adminRoles.deleteGroup(path2[2]);
|
|
8846
|
+
if (path2.length === 4 && path2[3] === "permissions" && m === "PUT") return adminRoles.putPermissions(req, path2[2]);
|
|
7494
8847
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
7495
8848
|
}
|
|
7496
|
-
if (
|
|
8849
|
+
if (path2[0] === "dashboard" && path2[1] === "stats" && path2.length === 2 && m === "GET" && dashboardGet) {
|
|
7497
8850
|
return dashboardGet(req);
|
|
7498
8851
|
}
|
|
7499
|
-
if (
|
|
8852
|
+
if (path2[0] === "dashboard" && path2[1] === "ecommerce" && path2.length === 2 && m === "GET" && ecommerceAnalyticsGet) {
|
|
7500
8853
|
const g = await analyticsGate();
|
|
7501
8854
|
if (g) return g;
|
|
7502
8855
|
return ecommerceAnalyticsGet(req);
|
|
7503
8856
|
}
|
|
7504
|
-
if (
|
|
7505
|
-
|
|
8857
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 3 && m === "DELETE" && getCms) {
|
|
8858
|
+
const id = path2[2];
|
|
8859
|
+
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);
|
|
8860
|
+
const a = await config.requireAuth(req);
|
|
8861
|
+
if (a) return a;
|
|
8862
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8863
|
+
if (pe) return pe;
|
|
8864
|
+
if (!entityMap.rss_feeds) {
|
|
8865
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8866
|
+
}
|
|
8867
|
+
if (!uuidLooksValid) {
|
|
8868
|
+
return config.json({ error: "Invalid feed id" }, { status: 400 });
|
|
8869
|
+
}
|
|
8870
|
+
try {
|
|
8871
|
+
await dataSource.getRepository(entityMap.rss_feeds).delete({ id });
|
|
8872
|
+
return config.json({ ok: true });
|
|
8873
|
+
} catch (e) {
|
|
8874
|
+
const message = e instanceof Error ? e.message : "Delete failed";
|
|
8875
|
+
return config.json({ error: message }, { status: 500 });
|
|
8876
|
+
}
|
|
8877
|
+
}
|
|
8878
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 2 && m === "GET" && getCms) {
|
|
8879
|
+
const a = await config.requireAuth(req);
|
|
8880
|
+
if (a) return a;
|
|
8881
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8882
|
+
if (pe) return pe;
|
|
8883
|
+
if (!entityMap.rss_feeds) {
|
|
8884
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8885
|
+
}
|
|
8886
|
+
try {
|
|
8887
|
+
const list = await dataSource.getRepository(entityMap.rss_feeds).find({
|
|
8888
|
+
where: { isActive: true },
|
|
8889
|
+
order: { updatedAt: "DESC" }
|
|
8890
|
+
});
|
|
8891
|
+
return config.json({ feeds: list.map(serializeRssFeed) });
|
|
8892
|
+
} catch (e) {
|
|
8893
|
+
const message = e instanceof Error ? e.message : "Failed to list feeds";
|
|
8894
|
+
return config.json({ error: message }, { status: 500 });
|
|
8895
|
+
}
|
|
8896
|
+
}
|
|
8897
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 2 && m === "POST" && getCms) {
|
|
8898
|
+
const a = await config.requireAuth(req);
|
|
8899
|
+
if (a) return a;
|
|
8900
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8901
|
+
if (pe) return pe;
|
|
8902
|
+
if (!entityMap.rss_feeds) {
|
|
8903
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8904
|
+
}
|
|
8905
|
+
let body;
|
|
8906
|
+
try {
|
|
8907
|
+
body = await req.json();
|
|
8908
|
+
} catch {
|
|
8909
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8910
|
+
}
|
|
8911
|
+
const rowsIn = Array.isArray(body.rows) ? body.rows : [];
|
|
8912
|
+
const rows = rowsIn.map((r) => r).map((r) => {
|
|
8913
|
+
let websiteUrl;
|
|
8914
|
+
if ("websiteUrl" in r) {
|
|
8915
|
+
const w = r.websiteUrl;
|
|
8916
|
+
if (w === null || w === "") websiteUrl = null;
|
|
8917
|
+
else if (typeof w === "string") websiteUrl = w;
|
|
8918
|
+
}
|
|
8919
|
+
return {
|
|
8920
|
+
rssUrl: typeof r.rssUrl === "string" ? r.rssUrl.trim() : "",
|
|
8921
|
+
name: typeof r.name === "string" ? r.name : void 0,
|
|
8922
|
+
websiteUrl
|
|
8923
|
+
};
|
|
8924
|
+
}).filter((r) => r.rssUrl !== "");
|
|
8925
|
+
try {
|
|
8926
|
+
const Feed = entityMap.rss_feeds;
|
|
8927
|
+
for (const r of rows) {
|
|
8928
|
+
await upsertRssFeedRow(dataSource, Feed, r.rssUrl, {
|
|
8929
|
+
name: r.name,
|
|
8930
|
+
websiteUrl: r.websiteUrl
|
|
8931
|
+
});
|
|
8932
|
+
}
|
|
8933
|
+
const list = await dataSource.getRepository(Feed).find({
|
|
8934
|
+
where: { isActive: true },
|
|
8935
|
+
order: { updatedAt: "DESC" }
|
|
8936
|
+
});
|
|
8937
|
+
return config.json({ feeds: list.map(serializeRssFeed) });
|
|
8938
|
+
} catch (e) {
|
|
8939
|
+
const message = e instanceof Error ? e.message : "Failed to save feeds";
|
|
8940
|
+
return config.json({ error: message }, { status: 500 });
|
|
8941
|
+
}
|
|
8942
|
+
}
|
|
8943
|
+
if (path2[0] === "blog-generator" && path2[1] === "latest" && path2.length === 2 && m === "POST" && getCms) {
|
|
8944
|
+
const a = await config.requireAuth(req);
|
|
8945
|
+
if (a) return a;
|
|
8946
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8947
|
+
if (pe) return pe;
|
|
8948
|
+
const cms = await getCms();
|
|
8949
|
+
const svc = cms.getPlugin("blog_generator");
|
|
8950
|
+
if (!svc) {
|
|
8951
|
+
return config.json({ error: "Blog Generator plugin is not enabled" }, { status: 503 });
|
|
8952
|
+
}
|
|
8953
|
+
let body;
|
|
8954
|
+
try {
|
|
8955
|
+
body = await req.json();
|
|
8956
|
+
} catch {
|
|
8957
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8958
|
+
}
|
|
8959
|
+
const rawUrls = [];
|
|
8960
|
+
if (Array.isArray(body.rssUrls)) {
|
|
8961
|
+
for (const u of body.rssUrls) {
|
|
8962
|
+
if (typeof u === "string") {
|
|
8963
|
+
const t = u.trim();
|
|
8964
|
+
if (t) rawUrls.push(t);
|
|
8965
|
+
}
|
|
8966
|
+
}
|
|
8967
|
+
}
|
|
8968
|
+
if (rawUrls.length === 0 && typeof body.rssUrl === "string" && body.rssUrl.trim()) {
|
|
8969
|
+
rawUrls.push(body.rssUrl.trim());
|
|
8970
|
+
}
|
|
8971
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8972
|
+
const urls = rawUrls.filter((u) => {
|
|
8973
|
+
if (seen.has(u)) return false;
|
|
8974
|
+
seen.add(u);
|
|
8975
|
+
return true;
|
|
8976
|
+
});
|
|
8977
|
+
if (urls.length === 0) {
|
|
8978
|
+
return config.json({ error: "Provide rssUrls (array) or rssUrl (string)" }, { status: 400 });
|
|
8979
|
+
}
|
|
8980
|
+
const results = [];
|
|
8981
|
+
for (const rssUrl of urls) {
|
|
8982
|
+
try {
|
|
8983
|
+
if (entityMap.rss_feeds) {
|
|
8984
|
+
await upsertRssFeedRow(dataSource, entityMap.rss_feeds, rssUrl);
|
|
8985
|
+
}
|
|
8986
|
+
const latest = await svc.getLatestArticleFromFeed(rssUrl);
|
|
8987
|
+
if (entityMap.rss_feeds) {
|
|
8988
|
+
await recordRssFetchSuccess(
|
|
8989
|
+
dataSource,
|
|
8990
|
+
entityMap.rss_feeds,
|
|
8991
|
+
entityMap.rss_articles,
|
|
8992
|
+
rssUrl,
|
|
8993
|
+
latest == null ? null : {
|
|
8994
|
+
title: latest.title,
|
|
8995
|
+
link: latest.link,
|
|
8996
|
+
summary: latest.summary,
|
|
8997
|
+
content: latest.content,
|
|
8998
|
+
creator: latest.creator,
|
|
8999
|
+
date: latest.date
|
|
9000
|
+
}
|
|
9001
|
+
);
|
|
9002
|
+
}
|
|
9003
|
+
const article = latest == null ? null : {
|
|
9004
|
+
title: latest.title,
|
|
9005
|
+
link: latest.link,
|
|
9006
|
+
summary: latest.summary,
|
|
9007
|
+
content: latest.content,
|
|
9008
|
+
date: latest.date.toISOString()
|
|
9009
|
+
};
|
|
9010
|
+
results.push({ rssUrl, article });
|
|
9011
|
+
} catch (e) {
|
|
9012
|
+
const message = e instanceof Error ? e.message : "Failed to parse feed";
|
|
9013
|
+
results.push({ rssUrl, article: null, error: message });
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
const singleArticle = results.length === 1 ? results[0].article : void 0;
|
|
9017
|
+
return config.json({ results, article: singleArticle });
|
|
9018
|
+
}
|
|
9019
|
+
if (path2[0] === "blog-generator" && path2[1] === "generate" && path2.length === 2 && m === "POST" && getCms) {
|
|
9020
|
+
const a = await config.requireAuth(req);
|
|
9021
|
+
if (a) return a;
|
|
9022
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
9023
|
+
if (pe) return pe;
|
|
9024
|
+
const cms = await getCms();
|
|
9025
|
+
const svc = cms.getPlugin("blog_generator");
|
|
9026
|
+
if (!svc) {
|
|
9027
|
+
return config.json({ error: "Blog Generator plugin is not enabled" }, { status: 503 });
|
|
9028
|
+
}
|
|
9029
|
+
const llm = cms.getPlugin("llm");
|
|
9030
|
+
if (!llm) {
|
|
9031
|
+
return config.json({ error: "LLM plugin is not enabled. Configure the LLM gateway to generate articles." }, { status: 503 });
|
|
9032
|
+
}
|
|
9033
|
+
let body;
|
|
9034
|
+
try {
|
|
9035
|
+
body = await req.json();
|
|
9036
|
+
} catch {
|
|
9037
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9038
|
+
}
|
|
9039
|
+
const rawUrls = [];
|
|
9040
|
+
if (Array.isArray(body.rssUrls)) {
|
|
9041
|
+
for (const u of body.rssUrls) {
|
|
9042
|
+
if (typeof u === "string") {
|
|
9043
|
+
const t = u.trim();
|
|
9044
|
+
if (t) rawUrls.push(t);
|
|
9045
|
+
}
|
|
9046
|
+
}
|
|
9047
|
+
}
|
|
9048
|
+
if (rawUrls.length === 0 && typeof body.rssUrl === "string" && body.rssUrl.trim()) {
|
|
9049
|
+
rawUrls.push(body.rssUrl.trim());
|
|
9050
|
+
}
|
|
9051
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9052
|
+
const rssUrls = rawUrls.filter((u) => {
|
|
9053
|
+
if (seen.has(u)) return false;
|
|
9054
|
+
seen.add(u);
|
|
9055
|
+
return true;
|
|
9056
|
+
});
|
|
9057
|
+
if (rssUrls.length === 0) {
|
|
9058
|
+
return config.json({ error: "Provide rssUrls (non-empty array) or rssUrl (string)" }, { status: 400 });
|
|
9059
|
+
}
|
|
9060
|
+
const systemInstruction = typeof body.systemInstruction === "string" ? body.systemInstruction : void 0;
|
|
9061
|
+
const validationRules = typeof body.validationRules === "string" ? body.validationRules : void 0;
|
|
9062
|
+
const persistToCms = body.persistToCms === true;
|
|
9063
|
+
let llmAgentChatOptions;
|
|
9064
|
+
let llmAgentResolution = null;
|
|
9065
|
+
let metadataLlmAgentChatOptions;
|
|
9066
|
+
let metadataLlmAgentResolution = null;
|
|
9067
|
+
let socialLlmAgentChatOptions;
|
|
9068
|
+
let socialLlmAgentResolution = null;
|
|
9069
|
+
let metadataRow = null;
|
|
9070
|
+
let socialRow = null;
|
|
9071
|
+
if (entityMap.llm_agents) {
|
|
9072
|
+
const agentRepo = dataSource.getRepository(entityMap.llm_agents);
|
|
9073
|
+
const agentRow = await agentRepo.findOne({
|
|
9074
|
+
where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9075
|
+
});
|
|
9076
|
+
if (agentRow) {
|
|
9077
|
+
const o = llmAgentToChatAgentOptions(agentRow);
|
|
9078
|
+
llmAgentChatOptions = {
|
|
9079
|
+
model: o.model,
|
|
9080
|
+
temperature: o.temperature,
|
|
9081
|
+
max_tokens: o.max_tokens
|
|
9082
|
+
};
|
|
9083
|
+
llmAgentResolution = {
|
|
9084
|
+
slug: BLOG_GENERATOR_LLM_AGENT_SLUG,
|
|
9085
|
+
model: agentRow.model?.trim() || null,
|
|
9086
|
+
temperature: agentRow.temperature ?? null,
|
|
9087
|
+
maxTokens: agentRow.maxTokens ?? null
|
|
9088
|
+
};
|
|
9089
|
+
}
|
|
9090
|
+
metadataRow = await agentRepo.findOne({
|
|
9091
|
+
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9092
|
+
});
|
|
9093
|
+
if (metadataRow) {
|
|
9094
|
+
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
9095
|
+
metadataLlmAgentChatOptions = {
|
|
9096
|
+
model: mo.model,
|
|
9097
|
+
temperature: mo.temperature,
|
|
9098
|
+
max_tokens: mo.max_tokens
|
|
9099
|
+
};
|
|
9100
|
+
metadataLlmAgentResolution = {
|
|
9101
|
+
slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG,
|
|
9102
|
+
model: metadataRow.model?.trim() || null,
|
|
9103
|
+
temperature: metadataRow.temperature ?? null,
|
|
9104
|
+
maxTokens: metadataRow.maxTokens ?? null
|
|
9105
|
+
};
|
|
9106
|
+
}
|
|
9107
|
+
socialRow = await agentRepo.findOne({
|
|
9108
|
+
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9109
|
+
});
|
|
9110
|
+
if (socialRow) {
|
|
9111
|
+
const so = llmAgentToChatAgentOptions(socialRow);
|
|
9112
|
+
socialLlmAgentChatOptions = {
|
|
9113
|
+
model: so.model,
|
|
9114
|
+
temperature: so.temperature,
|
|
9115
|
+
max_tokens: so.max_tokens
|
|
9116
|
+
};
|
|
9117
|
+
socialLlmAgentResolution = {
|
|
9118
|
+
slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG,
|
|
9119
|
+
model: socialRow.model?.trim() || null,
|
|
9120
|
+
temperature: socialRow.temperature ?? null,
|
|
9121
|
+
maxTokens: socialRow.maxTokens ?? null
|
|
9122
|
+
};
|
|
9123
|
+
}
|
|
9124
|
+
}
|
|
9125
|
+
try {
|
|
9126
|
+
const categoryEntities = entityMap.categories ? await dataSource.getRepository(entityMap.categories).find({
|
|
9127
|
+
where: { deleted: false },
|
|
9128
|
+
select: ["id", "name"]
|
|
9129
|
+
}) : [];
|
|
9130
|
+
const categoryRows = categoryEntities.map((r) => ({
|
|
9131
|
+
id: Number(r.id),
|
|
9132
|
+
name: String(r.name ?? "").trim()
|
|
9133
|
+
})).filter((r) => Number.isFinite(r.id) && r.name !== "");
|
|
9134
|
+
const tagEntities = entityMap.tags ? await dataSource.getRepository(entityMap.tags).find({
|
|
9135
|
+
where: { deleted: false },
|
|
9136
|
+
select: ["name"]
|
|
9137
|
+
}) : [];
|
|
9138
|
+
const tagNames = tagEntities.map((r) => String(r.name ?? "").trim()).filter((n) => n !== "");
|
|
9139
|
+
const out = await svc.generateBlogMarkdownFromRss({
|
|
9140
|
+
llm,
|
|
9141
|
+
rssUrls,
|
|
9142
|
+
systemInstruction,
|
|
9143
|
+
validationRules,
|
|
9144
|
+
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
9145
|
+
tagNamesHint: tagNames,
|
|
9146
|
+
llmAgentChatOptions,
|
|
9147
|
+
metadataSystemInstruction: metadataRow?.systemInstruction,
|
|
9148
|
+
metadataValidationRules: metadataRow?.validationRules ?? void 0,
|
|
9149
|
+
metadataLlmAgentChatOptions,
|
|
9150
|
+
socialSystemInstruction: socialRow?.systemInstruction,
|
|
9151
|
+
socialValidationRules: socialRow?.validationRules ?? void 0,
|
|
9152
|
+
socialLlmAgentChatOptions
|
|
9153
|
+
});
|
|
9154
|
+
let savedBlogs;
|
|
9155
|
+
if (persistToCms) {
|
|
9156
|
+
const pCreate = await requireEntityPermissionEffective(req, "blogs", "create");
|
|
9157
|
+
if (pCreate) return pCreate;
|
|
9158
|
+
if (!entityMap.blogs || !entityMap.tags || !entityMap.categories || !entityMap.seos) {
|
|
9159
|
+
return config.json(
|
|
9160
|
+
{ error: "persistToCms requires blogs, tags, categories, and seos in the entity map." },
|
|
9161
|
+
{ status: 503 }
|
|
9162
|
+
);
|
|
9163
|
+
}
|
|
9164
|
+
let authorId = null;
|
|
9165
|
+
if (getSessionUser) {
|
|
9166
|
+
const su = await getSessionUser();
|
|
9167
|
+
if (su?.id) {
|
|
9168
|
+
const n = Number(su.id);
|
|
9169
|
+
if (Number.isFinite(n)) authorId = n;
|
|
9170
|
+
}
|
|
9171
|
+
if (authorId == null && su?.email?.trim() && entityMap.users) {
|
|
9172
|
+
const ur = await dataSource.getRepository(entityMap.users).findOne({
|
|
9173
|
+
where: { email: su.email.trim(), deleted: false },
|
|
9174
|
+
select: ["id"]
|
|
9175
|
+
});
|
|
9176
|
+
if (ur) authorId = Number(ur.id);
|
|
9177
|
+
}
|
|
9178
|
+
}
|
|
9179
|
+
if (authorId == null || !Number.isFinite(authorId)) {
|
|
9180
|
+
return config.json(
|
|
9181
|
+
{
|
|
9182
|
+
error: "persistToCms requires a logged-in user with id in session or a users row matching session email."
|
|
9183
|
+
},
|
|
9184
|
+
{ status: 400 }
|
|
9185
|
+
);
|
|
9186
|
+
}
|
|
9187
|
+
savedBlogs = await persistAllGeneratedBlogDrafts(
|
|
9188
|
+
dataSource,
|
|
9189
|
+
{
|
|
9190
|
+
blogs: entityMap.blogs,
|
|
9191
|
+
tags: entityMap.tags,
|
|
9192
|
+
categories: entityMap.categories,
|
|
9193
|
+
seos: entityMap.seos
|
|
9194
|
+
},
|
|
9195
|
+
{ drafts: out.blogDrafts, authorId }
|
|
9196
|
+
);
|
|
9197
|
+
}
|
|
9198
|
+
const blogs = out.blogDrafts.map((draft) => {
|
|
9199
|
+
const { categoryId, matched: categoryMatched } = resolveBlogCategoryIdByName(
|
|
9200
|
+
draft.categoryName,
|
|
9201
|
+
categoryRows
|
|
9202
|
+
);
|
|
9203
|
+
return {
|
|
9204
|
+
title: draft.title,
|
|
9205
|
+
slug: draft.slug ?? null,
|
|
9206
|
+
markdown: draft.markdown,
|
|
9207
|
+
socialMediaContent: draft.socialMediaContent ?? null,
|
|
9208
|
+
categoryName: draft.categoryName,
|
|
9209
|
+
categoryId,
|
|
9210
|
+
categoryMatched,
|
|
9211
|
+
seo: draft.seo,
|
|
9212
|
+
tags: draft.tags,
|
|
9213
|
+
parseMode: draft.parseMode
|
|
9214
|
+
};
|
|
9215
|
+
});
|
|
9216
|
+
const firstArticle = out.article;
|
|
9217
|
+
return config.json({
|
|
9218
|
+
agentName: out.agentName,
|
|
9219
|
+
blogMarkdown: out.blogMarkdown,
|
|
9220
|
+
blogs,
|
|
9221
|
+
blog: blogs[0] ?? null,
|
|
9222
|
+
feedArticles: out.feedArticles.map((f) => ({
|
|
9223
|
+
rssUrl: f.rssUrl,
|
|
9224
|
+
article: {
|
|
9225
|
+
title: f.article.title,
|
|
9226
|
+
link: f.article.link,
|
|
9227
|
+
summary: f.article.summary,
|
|
9228
|
+
content: f.article.content,
|
|
9229
|
+
date: f.article.date.toISOString()
|
|
9230
|
+
}
|
|
9231
|
+
})),
|
|
9232
|
+
llmAgent: llmAgentResolution,
|
|
9233
|
+
metadataLlmAgent: metadataLlmAgentResolution,
|
|
9234
|
+
socialLlmAgent: socialLlmAgentResolution,
|
|
9235
|
+
article: {
|
|
9236
|
+
title: firstArticle.title,
|
|
9237
|
+
link: firstArticle.link,
|
|
9238
|
+
summary: firstArticle.summary,
|
|
9239
|
+
content: firstArticle.content,
|
|
9240
|
+
date: firstArticle.date.toISOString()
|
|
9241
|
+
},
|
|
9242
|
+
...savedBlogs != null ? { savedBlogs } : {}
|
|
9243
|
+
});
|
|
9244
|
+
} catch (e) {
|
|
9245
|
+
const message = e instanceof Error ? e.message : "Failed to generate blog";
|
|
9246
|
+
const status = /No items found|LLM returned empty/i.test(message) ? 400 : 502;
|
|
9247
|
+
return config.json({ error: message }, { status });
|
|
9248
|
+
}
|
|
9249
|
+
}
|
|
9250
|
+
if (path2[0] === "analytics" && analyticsHandlers) {
|
|
9251
|
+
if (path2.length === 1 && m === "GET") {
|
|
7506
9252
|
const g = await analyticsGate();
|
|
7507
9253
|
if (g) return g;
|
|
7508
9254
|
return analyticsHandlers.GET(req);
|
|
7509
9255
|
}
|
|
7510
|
-
if (
|
|
9256
|
+
if (path2.length === 2 && path2[1] === "property-id" && m === "GET") {
|
|
7511
9257
|
const g = await analyticsGate();
|
|
7512
9258
|
if (g) return g;
|
|
7513
9259
|
return analyticsHandlers.propertyId();
|
|
7514
9260
|
}
|
|
7515
|
-
if (
|
|
9261
|
+
if (path2.length === 2 && path2[1] === "permissions" && m === "GET") {
|
|
7516
9262
|
const g = await analyticsGate();
|
|
7517
9263
|
if (g) return g;
|
|
7518
9264
|
return analyticsHandlers.permissions();
|
|
7519
9265
|
}
|
|
7520
9266
|
}
|
|
7521
|
-
if (
|
|
7522
|
-
if (
|
|
7523
|
-
return zipExtractPost(req,
|
|
9267
|
+
if (path2[0] === "upload" && path2.length === 1 && m === "POST" && uploadPost) return uploadPost(req);
|
|
9268
|
+
if (path2[0] === "media" && path2[1] === "extract" && path2.length === 3 && m === "POST" && zipExtractPost) {
|
|
9269
|
+
return zipExtractPost(req, path2[2]);
|
|
7524
9270
|
}
|
|
7525
|
-
if (
|
|
7526
|
-
return blogBySlugGet(req,
|
|
9271
|
+
if (path2[0] === "blogs" && path2[1] === "slug" && path2.length === 3 && m === "GET" && blogBySlugGet) {
|
|
9272
|
+
return blogBySlugGet(req, path2[2]);
|
|
7527
9273
|
}
|
|
7528
|
-
if (
|
|
7529
|
-
return formBySlugGet(req,
|
|
9274
|
+
if (path2[0] === "forms" && path2[1] === "slug" && path2.length === 3 && m === "GET" && formBySlugGet) {
|
|
9275
|
+
return formBySlugGet(req, path2[2]);
|
|
7530
9276
|
}
|
|
7531
|
-
if (
|
|
7532
|
-
if (
|
|
9277
|
+
if (path2[0] === "form-submissions") {
|
|
9278
|
+
if (path2.length === 1) {
|
|
7533
9279
|
if (m === "GET" && formSubmissionList) return formSubmissionList(req);
|
|
7534
9280
|
if (m === "POST" && formSubmissionPost) return formSubmissionPost(req);
|
|
7535
9281
|
}
|
|
7536
|
-
if (
|
|
9282
|
+
if (path2.length === 2 && m === "GET" && formSubmissionGetById) return formSubmissionGetById(req, path2[1]);
|
|
7537
9283
|
}
|
|
7538
|
-
if (
|
|
7539
|
-
if (
|
|
7540
|
-
if (
|
|
7541
|
-
if (m === "GET") return formSaveHandlers.GET(req,
|
|
7542
|
-
if (m === "PUT" || m === "PATCH") return formSaveHandlers.PUT(req,
|
|
9284
|
+
if (path2[0] === "forms" && formSaveHandlers) {
|
|
9285
|
+
if (path2.length === 1 && m === "POST") return formSaveHandlers.POST(req);
|
|
9286
|
+
if (path2.length === 2) {
|
|
9287
|
+
if (m === "GET") return formSaveHandlers.GET(req, path2[1]);
|
|
9288
|
+
if (m === "PUT" || m === "PATCH") return formSaveHandlers.PUT(req, path2[1]);
|
|
7543
9289
|
}
|
|
7544
9290
|
}
|
|
7545
|
-
if (
|
|
7546
|
-
if (
|
|
9291
|
+
if (path2[0] === "users" && usersHandlers) {
|
|
9292
|
+
if (path2.length === 1) {
|
|
7547
9293
|
if (m === "GET") return usersHandlers.list(req);
|
|
7548
9294
|
if (m === "POST") return usersHandlers.create(req);
|
|
7549
9295
|
}
|
|
7550
|
-
if (
|
|
7551
|
-
if (
|
|
7552
|
-
if (
|
|
9296
|
+
if (path2.length === 2) {
|
|
9297
|
+
if (path2[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
|
|
9298
|
+
if (path2[1] === "profile" && profileHandlers) {
|
|
7553
9299
|
if (m === "GET") return profileHandlers.GET(req);
|
|
7554
9300
|
if (m === "PUT") return profileHandlers.PUT(req);
|
|
7555
9301
|
}
|
|
7556
|
-
const id =
|
|
9302
|
+
const id = path2[1];
|
|
7557
9303
|
if (m === "GET") return usersHandlers.getById(req, id);
|
|
7558
9304
|
if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
|
|
7559
9305
|
if (m === "DELETE") return usersHandlers.delete(req, id);
|
|
7560
9306
|
}
|
|
7561
|
-
if (
|
|
7562
|
-
return usersHandlers.regenerateInvite(req,
|
|
9307
|
+
if (path2.length === 3 && path2[2] === "regenerate-invite" && m === "POST") {
|
|
9308
|
+
return usersHandlers.regenerateInvite(req, path2[1]);
|
|
7563
9309
|
}
|
|
7564
9310
|
}
|
|
7565
|
-
if (
|
|
7566
|
-
return userAuthRouter.POST(req,
|
|
9311
|
+
if (path2[0] === "users" && path2.length === 2 && userAuthRouter && m === "POST") {
|
|
9312
|
+
return userAuthRouter.POST(req, path2[1]);
|
|
7567
9313
|
}
|
|
7568
|
-
if (
|
|
7569
|
-
const
|
|
9314
|
+
if (path2[0] === "social-media" && path2[1] === "linkedin" && socialMediaHandlers && path2.length === 3) {
|
|
9315
|
+
const tail = path2[2];
|
|
9316
|
+
if (tail === "status" && m === "GET") {
|
|
9317
|
+
return socialMediaHandlers.getLinkedInStatus(req);
|
|
9318
|
+
}
|
|
9319
|
+
if (tail === "sync-profile" && m === "POST") {
|
|
9320
|
+
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
9321
|
+
}
|
|
9322
|
+
if (tail === "publish-blog" && m === "POST") {
|
|
9323
|
+
let body;
|
|
9324
|
+
try {
|
|
9325
|
+
body = await req.json();
|
|
9326
|
+
} catch {
|
|
9327
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9328
|
+
}
|
|
9329
|
+
const id = Number(body?.blogId);
|
|
9330
|
+
return socialMediaHandlers.publishBlogToLinkedIn(req, id);
|
|
9331
|
+
}
|
|
9332
|
+
}
|
|
9333
|
+
if (path2[0] === "social-media" && path2[1] === "facebook" && socialMediaHandlers && path2.length === 3) {
|
|
9334
|
+
const tail = path2[2];
|
|
9335
|
+
if (tail === "fetch-pages" && m === "POST") {
|
|
9336
|
+
return socialMediaHandlers.fetchFacebookManagedPages(req);
|
|
9337
|
+
}
|
|
9338
|
+
if (tail === "status" && m === "GET") {
|
|
9339
|
+
return socialMediaHandlers.getFacebookStatus(req);
|
|
9340
|
+
}
|
|
9341
|
+
if (tail === "publish-blog" && m === "POST") {
|
|
9342
|
+
let body;
|
|
9343
|
+
try {
|
|
9344
|
+
body = await req.json();
|
|
9345
|
+
} catch {
|
|
9346
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9347
|
+
}
|
|
9348
|
+
const id = Number(body?.blogId);
|
|
9349
|
+
return socialMediaHandlers.publishBlogToFacebook(req, id);
|
|
9350
|
+
}
|
|
9351
|
+
}
|
|
9352
|
+
if (path2[0] === "settings" && path2.length === 2 && settingsHandlers) {
|
|
9353
|
+
const group = path2[1];
|
|
7570
9354
|
const isPublic = settingsConfig?.publicGetGroups?.includes(group);
|
|
7571
9355
|
if (m === "GET") {
|
|
7572
9356
|
if (!isPublic) {
|
|
@@ -7583,15 +9367,15 @@ function createCmsApiHandler(config) {
|
|
|
7583
9367
|
return settingsHandlers.PUT(req, group);
|
|
7584
9368
|
}
|
|
7585
9369
|
}
|
|
7586
|
-
if (
|
|
9370
|
+
if (path2[0] === "message-templates" && path2[1] === "sms" && path2.length === 2) {
|
|
7587
9371
|
if (m === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
7588
9372
|
if (m === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
7589
9373
|
}
|
|
7590
|
-
if (
|
|
9374
|
+
if (path2[0] === "llm_agents") {
|
|
7591
9375
|
if (!entityMap.llm_agents) {
|
|
7592
9376
|
console.error(CMS_API_LOG, "llm_agents route: entity missing after merge", {
|
|
7593
9377
|
method: m,
|
|
7594
|
-
pathJoined:
|
|
9378
|
+
pathJoined: path2.join("/"),
|
|
7595
9379
|
entityMapKeyCount: Object.keys(entityMap).length
|
|
7596
9380
|
});
|
|
7597
9381
|
return config.json(
|
|
@@ -7599,19 +9383,19 @@ function createCmsApiHandler(config) {
|
|
|
7599
9383
|
{ status: 503 }
|
|
7600
9384
|
);
|
|
7601
9385
|
}
|
|
7602
|
-
if (
|
|
9386
|
+
if (path2.length === 1) {
|
|
7603
9387
|
if (m === "GET") return crud.GET(req, "llm_agents");
|
|
7604
9388
|
if (m === "POST") return crud.POST(req, "llm_agents");
|
|
7605
9389
|
}
|
|
7606
|
-
if (
|
|
7607
|
-
const id =
|
|
9390
|
+
if (path2.length === 2 && !path2[1]?.includes("knowledge")) {
|
|
9391
|
+
const id = path2[1];
|
|
7608
9392
|
if (m === "GET") return crudById.GET(req, "llm_agents", id);
|
|
7609
9393
|
if (m === "PUT" || m === "PATCH") return crudById.PUT(req, "llm_agents", id);
|
|
7610
9394
|
if (m === "DELETE") return crudById.DELETE(req, "llm_agents", id);
|
|
7611
9395
|
}
|
|
7612
9396
|
}
|
|
7613
9397
|
{
|
|
7614
|
-
const kbMatch = matchLlmAgentKnowledgeRoute(
|
|
9398
|
+
const kbMatch = matchLlmAgentKnowledgeRoute(path2);
|
|
7615
9399
|
if (kbMatch) {
|
|
7616
9400
|
if (!llmAgentKnowledgeHandlers) {
|
|
7617
9401
|
return config.json(
|
|
@@ -7634,29 +9418,29 @@ function createCmsApiHandler(config) {
|
|
|
7634
9418
|
}
|
|
7635
9419
|
}
|
|
7636
9420
|
}
|
|
7637
|
-
if (
|
|
7638
|
-
if (
|
|
7639
|
-
if (
|
|
7640
|
-
if (
|
|
7641
|
-
if (
|
|
9421
|
+
if (path2[0] === "chat" && chatHandlers) {
|
|
9422
|
+
if (path2.length === 2 && path2[1] === "config" && m === "GET") return chatHandlers.publicConfig(req);
|
|
9423
|
+
if (path2.length === 2 && path2[1] === "identify" && m === "POST") return chatHandlers.identify(req);
|
|
9424
|
+
if (path2.length === 4 && path2[1] === "conversations" && path2[3] === "messages" && m === "GET") return chatHandlers.getMessages(req, path2[2]);
|
|
9425
|
+
if (path2.length === 2 && path2[1] === "messages" && m === "POST") return chatHandlers.postMessage(req);
|
|
7642
9426
|
}
|
|
7643
|
-
if (
|
|
9427
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "invoice" && m === "GET" && getCms) {
|
|
7644
9428
|
const a = await config.requireAuth(req);
|
|
7645
9429
|
if (a) return a;
|
|
7646
9430
|
const pe = await requireEntityPermissionEffective(req, "orders", "read");
|
|
7647
9431
|
if (pe) return pe;
|
|
7648
9432
|
const cms = await getCms();
|
|
7649
9433
|
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
7650
|
-
const oid = Number(
|
|
9434
|
+
const oid = Number(path2[1]);
|
|
7651
9435
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
7652
9436
|
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
7653
9437
|
}
|
|
7654
|
-
if (
|
|
9438
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "repost-erp" && getCms) {
|
|
7655
9439
|
const a = await config.requireAuth(req);
|
|
7656
9440
|
if (a) return a;
|
|
7657
9441
|
const pe = await requireEntityPermissionEffective(req, "orders", m === "GET" ? "read" : "update");
|
|
7658
9442
|
if (pe) return pe;
|
|
7659
|
-
const oid = Number(
|
|
9443
|
+
const oid = Number(path2[1]);
|
|
7660
9444
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
7661
9445
|
const cms = await getCms();
|
|
7662
9446
|
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
@@ -7672,13 +9456,13 @@ function createCmsApiHandler(config) {
|
|
|
7672
9456
|
}
|
|
7673
9457
|
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
7674
9458
|
}
|
|
7675
|
-
if (
|
|
7676
|
-
const resource = resolveResource(
|
|
9459
|
+
if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
9460
|
+
const resource = resolveResource(path2[0]);
|
|
7677
9461
|
if (!crudResources.includes(resource)) {
|
|
7678
9462
|
console.warn(CMS_API_LOG, "generic CRUD gate: Invalid resource (not in crudResources)", {
|
|
7679
9463
|
method: m,
|
|
7680
|
-
pathJoined:
|
|
7681
|
-
pathSegments:
|
|
9464
|
+
pathJoined: path2.join("/"),
|
|
9465
|
+
pathSegments: path2,
|
|
7682
9466
|
resource,
|
|
7683
9467
|
hasLlmAgentsEntity: Boolean(entityMap.llm_agents),
|
|
7684
9468
|
crudResourcesHasResource: crudResources.includes(resource),
|
|
@@ -7686,24 +9470,24 @@ function createCmsApiHandler(config) {
|
|
|
7686
9470
|
});
|
|
7687
9471
|
return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
7688
9472
|
}
|
|
7689
|
-
if (
|
|
7690
|
-
if (
|
|
9473
|
+
if (path2.length === 2) {
|
|
9474
|
+
if (path2[1] === "metadata" && m === "GET") {
|
|
7691
9475
|
return crud.GET_METADATA(req, resource);
|
|
7692
9476
|
}
|
|
7693
|
-
if (
|
|
9477
|
+
if (path2[1] === "bulk" && m === "POST") {
|
|
7694
9478
|
return crud.BULK_POST(req, resource);
|
|
7695
9479
|
}
|
|
7696
|
-
if (
|
|
9480
|
+
if (path2[1] === "export" && m === "GET") {
|
|
7697
9481
|
return crud.GET_EXPORT(req, resource);
|
|
7698
9482
|
}
|
|
7699
9483
|
}
|
|
7700
|
-
if (
|
|
9484
|
+
if (path2.length === 1) {
|
|
7701
9485
|
if (m === "GET") return crud.GET(req, resource);
|
|
7702
9486
|
if (m === "POST") return crud.POST(req, resource);
|
|
7703
9487
|
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
7704
9488
|
}
|
|
7705
|
-
if (
|
|
7706
|
-
const id =
|
|
9489
|
+
if (path2.length === 2) {
|
|
9490
|
+
const id = path2[1];
|
|
7707
9491
|
if (m === "GET") return crudById.GET(req, resource, id);
|
|
7708
9492
|
if (m === "PUT" || m === "PATCH") return crudById.PUT(req, resource, id);
|
|
7709
9493
|
if (m === "DELETE") return crudById.DELETE(req, resource, id);
|
|
@@ -7715,7 +9499,7 @@ function createCmsApiHandler(config) {
|
|
|
7715
9499
|
}
|
|
7716
9500
|
|
|
7717
9501
|
// src/api/storefront-handlers.ts
|
|
7718
|
-
var
|
|
9502
|
+
var import_typeorm49 = require("typeorm");
|
|
7719
9503
|
|
|
7720
9504
|
// src/lib/is-valid-signup-email.ts
|
|
7721
9505
|
var MAX_EMAIL = 254;
|
|
@@ -7975,7 +9759,7 @@ async function queueSms(cms, payload) {
|
|
|
7975
9759
|
|
|
7976
9760
|
// src/lib/otp-challenge.ts
|
|
7977
9761
|
var import_crypto = require("crypto");
|
|
7978
|
-
var
|
|
9762
|
+
var import_typeorm48 = require("typeorm");
|
|
7979
9763
|
var OTP_TTL_MS = 10 * 60 * 1e3;
|
|
7980
9764
|
var MAX_SENDS_PER_HOUR = 5;
|
|
7981
9765
|
var MAX_VERIFY_ATTEMPTS = 8;
|
|
@@ -8009,7 +9793,7 @@ function normalizePhoneE164(raw, defaultCountryCode) {
|
|
|
8009
9793
|
async function countRecentOtpSends(dataSource, entityMap, purpose, identifier, since) {
|
|
8010
9794
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
8011
9795
|
return repo.count({
|
|
8012
|
-
where: { purpose, identifier, createdAt: (0,
|
|
9796
|
+
where: { purpose, identifier, createdAt: (0, import_typeorm48.MoreThan)(since) }
|
|
8013
9797
|
});
|
|
8014
9798
|
}
|
|
8015
9799
|
async function createOtpChallenge(dataSource, entityMap, input) {
|
|
@@ -8023,7 +9807,7 @@ async function createOtpChallenge(dataSource, entityMap, input) {
|
|
|
8023
9807
|
await repo.delete({
|
|
8024
9808
|
purpose,
|
|
8025
9809
|
identifier,
|
|
8026
|
-
consumedAt: (0,
|
|
9810
|
+
consumedAt: (0, import_typeorm48.IsNull)()
|
|
8027
9811
|
});
|
|
8028
9812
|
const expiresAt = new Date(Date.now() + OTP_TTL_MS);
|
|
8029
9813
|
const codeHash = hashOtpCode(code, purpose, identifier, pepper);
|
|
@@ -8044,7 +9828,7 @@ async function verifyAndConsumeOtpChallenge(dataSource, entityMap, input) {
|
|
|
8044
9828
|
const { purpose, identifier, code, pepper } = input;
|
|
8045
9829
|
const repo = dataSource.getRepository(entityMap.otp_challenges);
|
|
8046
9830
|
const row = await repo.findOne({
|
|
8047
|
-
where: { purpose, identifier, consumedAt: (0,
|
|
9831
|
+
where: { purpose, identifier, consumedAt: (0, import_typeorm48.IsNull)() },
|
|
8048
9832
|
order: { id: "DESC" }
|
|
8049
9833
|
});
|
|
8050
9834
|
if (!row) {
|
|
@@ -8278,7 +10062,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8278
10062
|
const u = await userRepo().findOne({ where: { id: userId } });
|
|
8279
10063
|
if (!u) return null;
|
|
8280
10064
|
const unclaimed = await contactRepo().findOne({
|
|
8281
|
-
where: { email: u.email, userId: (0,
|
|
10065
|
+
where: { email: u.email, userId: (0, import_typeorm49.IsNull)(), deleted: false }
|
|
8282
10066
|
});
|
|
8283
10067
|
if (unclaimed) {
|
|
8284
10068
|
await contactRepo().update(unclaimed.id, { userId });
|
|
@@ -8421,7 +10205,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8421
10205
|
};
|
|
8422
10206
|
}
|
|
8423
10207
|
return {
|
|
8424
|
-
async handle(method,
|
|
10208
|
+
async handle(method, path2, req) {
|
|
8425
10209
|
try {
|
|
8426
10210
|
let serializeAddress2 = function(a) {
|
|
8427
10211
|
return {
|
|
@@ -8437,7 +10221,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8437
10221
|
};
|
|
8438
10222
|
};
|
|
8439
10223
|
var serializeAddress = serializeAddress2;
|
|
8440
|
-
if (
|
|
10224
|
+
if (path2[0] === "products" && path2.length === 1 && method === "GET") {
|
|
8441
10225
|
const url = new URL(req.url || "", "http://localhost");
|
|
8442
10226
|
const collectionSlug = url.searchParams.get("collection")?.trim();
|
|
8443
10227
|
const collectionId = url.searchParams.get("collectionId");
|
|
@@ -8479,8 +10263,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8479
10263
|
...collectionFilter && { collection: collectionFilter }
|
|
8480
10264
|
});
|
|
8481
10265
|
}
|
|
8482
|
-
if (
|
|
8483
|
-
const idOrSlug =
|
|
10266
|
+
if (path2[0] === "products" && path2.length === 2 && method === "GET") {
|
|
10267
|
+
const idOrSlug = path2[1];
|
|
8484
10268
|
const byId = /^\d+$/.test(idOrSlug);
|
|
8485
10269
|
const product = await productRepo().findOne({
|
|
8486
10270
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
@@ -8495,7 +10279,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8495
10279
|
})).filter((t) => t.name || t.value);
|
|
8496
10280
|
return json({ ...serializeProduct(p), attributes: attributeTags });
|
|
8497
10281
|
}
|
|
8498
|
-
if (
|
|
10282
|
+
if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
|
|
8499
10283
|
const items = await collectionRepo().find({
|
|
8500
10284
|
where: { active: true, deleted: false },
|
|
8501
10285
|
order: { sortOrder: "ASC", id: "ASC" }
|
|
@@ -8524,8 +10308,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8524
10308
|
})
|
|
8525
10309
|
});
|
|
8526
10310
|
}
|
|
8527
|
-
if (
|
|
8528
|
-
const idOrSlug =
|
|
10311
|
+
if (path2[0] === "collections" && path2.length === 2 && method === "GET") {
|
|
10312
|
+
const idOrSlug = path2[1];
|
|
8529
10313
|
const byId = /^\d+$/.test(idOrSlug);
|
|
8530
10314
|
const collection = await collectionRepo().findOne({
|
|
8531
10315
|
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
@@ -8548,7 +10332,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8548
10332
|
products: products.map((p) => serializeProduct(p))
|
|
8549
10333
|
});
|
|
8550
10334
|
}
|
|
8551
|
-
if (
|
|
10335
|
+
if (path2[0] === "profile" && path2.length === 1 && method === "GET") {
|
|
8552
10336
|
const u = await getSessionUser();
|
|
8553
10337
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8554
10338
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -8565,7 +10349,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8565
10349
|
} : null
|
|
8566
10350
|
});
|
|
8567
10351
|
}
|
|
8568
|
-
if (
|
|
10352
|
+
if (path2[0] === "profile" && path2.length === 1 && method === "PUT") {
|
|
8569
10353
|
const u = await getSessionUser();
|
|
8570
10354
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8571
10355
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -8602,7 +10386,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8602
10386
|
if (!contact) return json({ error: "Contact not found" }, { status: 404 });
|
|
8603
10387
|
return { contactId: contact.id };
|
|
8604
10388
|
}
|
|
8605
|
-
if (
|
|
10389
|
+
if (path2[0] === "addresses" && path2.length === 1 && method === "GET") {
|
|
8606
10390
|
const contactOrErr = await getContactForAddresses();
|
|
8607
10391
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8608
10392
|
const list = await addressRepo().find({
|
|
@@ -8611,7 +10395,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8611
10395
|
});
|
|
8612
10396
|
return json({ addresses: list.map((a) => serializeAddress2(a)) });
|
|
8613
10397
|
}
|
|
8614
|
-
if (
|
|
10398
|
+
if (path2[0] === "addresses" && path2.length === 1 && method === "POST") {
|
|
8615
10399
|
const contactOrErr = await getContactForAddresses();
|
|
8616
10400
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8617
10401
|
const b = await req.json().catch(() => ({}));
|
|
@@ -8630,10 +10414,10 @@ function createStorefrontApiHandler(config) {
|
|
|
8630
10414
|
const created = await addressRepo().save(addressRepo().create(row));
|
|
8631
10415
|
return json(serializeAddress2(created));
|
|
8632
10416
|
}
|
|
8633
|
-
if (
|
|
10417
|
+
if (path2[0] === "addresses" && path2.length === 2 && (method === "PATCH" || method === "PUT")) {
|
|
8634
10418
|
const contactOrErr = await getContactForAddresses();
|
|
8635
10419
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8636
|
-
const id = parseInt(
|
|
10420
|
+
const id = parseInt(path2[1], 10);
|
|
8637
10421
|
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
8638
10422
|
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
8639
10423
|
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
@@ -8659,17 +10443,17 @@ function createStorefrontApiHandler(config) {
|
|
|
8659
10443
|
const updated = await addressRepo().findOne({ where: { id } });
|
|
8660
10444
|
return json(serializeAddress2(updated));
|
|
8661
10445
|
}
|
|
8662
|
-
if (
|
|
10446
|
+
if (path2[0] === "addresses" && path2.length === 2 && method === "DELETE") {
|
|
8663
10447
|
const contactOrErr = await getContactForAddresses();
|
|
8664
10448
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8665
|
-
const id = parseInt(
|
|
10449
|
+
const id = parseInt(path2[1], 10);
|
|
8666
10450
|
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
8667
10451
|
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
8668
10452
|
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
8669
10453
|
await addressRepo().delete(id);
|
|
8670
10454
|
return json({ deleted: true });
|
|
8671
10455
|
}
|
|
8672
|
-
if (
|
|
10456
|
+
if (path2[0] === "verify-email" && path2.length === 1 && method === "POST") {
|
|
8673
10457
|
const b = await req.json().catch(() => ({}));
|
|
8674
10458
|
const token = typeof b.token === "string" ? b.token.trim() : "";
|
|
8675
10459
|
if (!token) return json({ error: "token is required" }, { status: 400 });
|
|
@@ -8688,7 +10472,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8688
10472
|
await tokenRepo().delete({ email });
|
|
8689
10473
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
8690
10474
|
}
|
|
8691
|
-
if (
|
|
10475
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "send" && path2.length === 3 && method === "POST") {
|
|
8692
10476
|
const b = await req.json().catch(() => ({}));
|
|
8693
10477
|
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
8694
10478
|
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
@@ -8773,7 +10557,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8773
10557
|
}
|
|
8774
10558
|
return json({ ok: true });
|
|
8775
10559
|
}
|
|
8776
|
-
if (
|
|
10560
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "verify-email" && path2.length === 3 && method === "POST") {
|
|
8777
10561
|
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
8778
10562
|
const b = await req.json().catch(() => ({}));
|
|
8779
10563
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
@@ -8796,7 +10580,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8796
10580
|
await tokenRepo().delete({ email });
|
|
8797
10581
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
8798
10582
|
}
|
|
8799
|
-
if (
|
|
10583
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "verify-phone" && path2.length === 3 && method === "POST") {
|
|
8800
10584
|
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
8801
10585
|
const su = await getSessionUser();
|
|
8802
10586
|
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
@@ -8824,7 +10608,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8824
10608
|
}
|
|
8825
10609
|
return json({ success: true });
|
|
8826
10610
|
}
|
|
8827
|
-
if (
|
|
10611
|
+
if (path2[0] === "register" && path2.length === 1 && method === "POST") {
|
|
8828
10612
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
8829
10613
|
const b = await req.json().catch(() => ({}));
|
|
8830
10614
|
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
@@ -8881,14 +10665,14 @@ function createStorefrontApiHandler(config) {
|
|
|
8881
10665
|
emailVerificationSent
|
|
8882
10666
|
});
|
|
8883
10667
|
}
|
|
8884
|
-
if (
|
|
10668
|
+
if (path2[0] === "cart" && path2.length === 1 && method === "GET") {
|
|
8885
10669
|
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
8886
10670
|
if (err) return err;
|
|
8887
10671
|
const body = serializeCart(cart);
|
|
8888
10672
|
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
8889
10673
|
return json(body);
|
|
8890
10674
|
}
|
|
8891
|
-
if (
|
|
10675
|
+
if (path2[0] === "cart" && path2[1] === "items" && path2.length === 2 && method === "POST") {
|
|
8892
10676
|
const body = await req.json().catch(() => ({}));
|
|
8893
10677
|
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
8894
10678
|
if (capCart) return capCart;
|
|
@@ -8919,8 +10703,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8919
10703
|
if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
|
|
8920
10704
|
return json(out);
|
|
8921
10705
|
}
|
|
8922
|
-
if (
|
|
8923
|
-
const itemId = parseInt(
|
|
10706
|
+
if (path2[0] === "cart" && path2[1] === "items" && path2.length === 3) {
|
|
10707
|
+
const itemId = parseInt(path2[2], 10);
|
|
8924
10708
|
if (!Number.isFinite(itemId)) return json({ error: "Invalid item id" }, { status: 400 });
|
|
8925
10709
|
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
8926
10710
|
if (err) return err;
|
|
@@ -8953,7 +10737,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8953
10737
|
return json(out);
|
|
8954
10738
|
}
|
|
8955
10739
|
}
|
|
8956
|
-
if (
|
|
10740
|
+
if (path2[0] === "cart" && path2[1] === "merge" && method === "POST") {
|
|
8957
10741
|
const u = await getSessionUser();
|
|
8958
10742
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8959
10743
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -9059,7 +10843,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9059
10843
|
}
|
|
9060
10844
|
return { wishlist: w, setCookie: null, err: null };
|
|
9061
10845
|
}
|
|
9062
|
-
if (
|
|
10846
|
+
if (path2[0] === "wishlist" && path2.length === 1 && method === "GET") {
|
|
9063
10847
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9064
10848
|
if (err) return err;
|
|
9065
10849
|
const items = await wishlistItemRepo().find({
|
|
@@ -9087,7 +10871,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9087
10871
|
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
9088
10872
|
return json(body);
|
|
9089
10873
|
}
|
|
9090
|
-
if (
|
|
10874
|
+
if (path2[0] === "wishlist" && path2[1] === "items" && path2.length === 2 && method === "POST") {
|
|
9091
10875
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9092
10876
|
if (err) return err;
|
|
9093
10877
|
const b = await req.json().catch(() => ({}));
|
|
@@ -9101,15 +10885,15 @@ function createStorefrontApiHandler(config) {
|
|
|
9101
10885
|
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
9102
10886
|
return json({ ok: true });
|
|
9103
10887
|
}
|
|
9104
|
-
if (
|
|
10888
|
+
if (path2[0] === "wishlist" && path2[1] === "items" && path2.length === 3 && method === "DELETE") {
|
|
9105
10889
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9106
10890
|
if (err) return err;
|
|
9107
|
-
const productId = parseInt(
|
|
10891
|
+
const productId = parseInt(path2[2], 10);
|
|
9108
10892
|
await wishlistItemRepo().delete({ wishlistId: wishlist.id, productId });
|
|
9109
10893
|
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
9110
10894
|
return json({ ok: true });
|
|
9111
10895
|
}
|
|
9112
|
-
if (
|
|
10896
|
+
if (path2[0] === "checkout" && path2[1] === "order" && path2.length === 2 && method === "POST") {
|
|
9113
10897
|
const b = await req.json().catch(() => ({}));
|
|
9114
10898
|
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
9115
10899
|
if (capOrd) return capOrd;
|
|
@@ -9212,7 +10996,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9212
10996
|
currency: cart.currency || "INR"
|
|
9213
10997
|
});
|
|
9214
10998
|
}
|
|
9215
|
-
if (
|
|
10999
|
+
if (path2[0] === "checkout" && path2.length === 1 && method === "POST") {
|
|
9216
11000
|
const b = await req.json().catch(() => ({}));
|
|
9217
11001
|
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
9218
11002
|
if (capChk) return capChk;
|
|
@@ -9314,7 +11098,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9314
11098
|
total: prepChk.orderTotal
|
|
9315
11099
|
});
|
|
9316
11100
|
}
|
|
9317
|
-
if (
|
|
11101
|
+
if (path2[0] === "orders" && path2.length === 1 && method === "GET") {
|
|
9318
11102
|
const u = await getSessionUser();
|
|
9319
11103
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9320
11104
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -9329,7 +11113,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9329
11113
|
const previewByOrder = {};
|
|
9330
11114
|
if (orderIds.length) {
|
|
9331
11115
|
const oItems = await orderItemRepo().find({
|
|
9332
|
-
where: { orderId: (0,
|
|
11116
|
+
where: { orderId: (0, import_typeorm49.In)(orderIds) },
|
|
9333
11117
|
relations: ["product"],
|
|
9334
11118
|
order: { id: "ASC" }
|
|
9335
11119
|
});
|
|
@@ -9356,27 +11140,27 @@ function createStorefrontApiHandler(config) {
|
|
|
9356
11140
|
})
|
|
9357
11141
|
});
|
|
9358
11142
|
}
|
|
9359
|
-
if (
|
|
11143
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "invoice" && method === "GET") {
|
|
9360
11144
|
const u = await getSessionUser();
|
|
9361
11145
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9362
11146
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
9363
11147
|
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
9364
11148
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
9365
11149
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
9366
|
-
const orderId = parseInt(
|
|
11150
|
+
const orderId = parseInt(path2[1], 10);
|
|
9367
11151
|
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
9368
11152
|
const cms = await getCms();
|
|
9369
11153
|
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
9370
11154
|
ownerContactId: contact.id
|
|
9371
11155
|
});
|
|
9372
11156
|
}
|
|
9373
|
-
if (
|
|
11157
|
+
if (path2[0] === "orders" && path2.length === 2 && method === "GET") {
|
|
9374
11158
|
const u = await getSessionUser();
|
|
9375
11159
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9376
11160
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
9377
11161
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
9378
11162
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
9379
|
-
const orderId = parseInt(
|
|
11163
|
+
const orderId = parseInt(path2[1], 10);
|
|
9380
11164
|
let order = await orderRepo().findOne({
|
|
9381
11165
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
9382
11166
|
relations: ["items", "items.product"]
|
|
@@ -9479,6 +11263,8 @@ function createStorefrontApiHandler(config) {
|
|
|
9479
11263
|
getPublicSettingsGroup,
|
|
9480
11264
|
mergeGuardrailsIntoSystemPrompt,
|
|
9481
11265
|
parseLlmAgentValidationRules,
|
|
11266
|
+
simpleDecrypt,
|
|
11267
|
+
simpleEncrypt,
|
|
9482
11268
|
validateUserMessageAgainstAgentRules,
|
|
9483
11269
|
validateUserMessageAgainstStructuredRules
|
|
9484
11270
|
});
|