@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.js
CHANGED
|
@@ -1906,14 +1906,14 @@ function createUserAuthApiRouter(config) {
|
|
|
1906
1906
|
}) : null;
|
|
1907
1907
|
return {
|
|
1908
1908
|
async POST(req, pathname) {
|
|
1909
|
-
const
|
|
1910
|
-
if (!USER_AUTH_PATHS.includes(
|
|
1909
|
+
const path2 = pathname.replace(/\/$/, "");
|
|
1910
|
+
if (!USER_AUTH_PATHS.includes(path2)) {
|
|
1911
1911
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
1912
1912
|
}
|
|
1913
|
-
if (
|
|
1914
|
-
if (
|
|
1915
|
-
if (
|
|
1916
|
-
if (
|
|
1913
|
+
if (path2 === "forgot-password") return forgot(req);
|
|
1914
|
+
if (path2 === "set-password") return setPass(req);
|
|
1915
|
+
if (path2 === "invite") return invite(req);
|
|
1916
|
+
if (path2 === "reset-password" && changePass) return changePass(req);
|
|
1917
1917
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
1918
1918
|
}
|
|
1919
1919
|
};
|
|
@@ -2182,12 +2182,12 @@ async function extractZipMediaIntoParentTree(opts) {
|
|
|
2182
2182
|
if (opts.storage) {
|
|
2183
2183
|
publicUrl = await opts.storage.upload(buf, `uploads/${relativeUnderUploads}`, contentType);
|
|
2184
2184
|
} else {
|
|
2185
|
-
const
|
|
2185
|
+
const fs2 = await import("fs/promises");
|
|
2186
2186
|
const pathMod = await import("path");
|
|
2187
2187
|
const dir = pathMod.join(process.cwd(), opts.localUploadDir);
|
|
2188
2188
|
const filePath = pathMod.join(dir, relativeUnderUploads);
|
|
2189
|
-
await
|
|
2190
|
-
await
|
|
2189
|
+
await fs2.mkdir(pathMod.dirname(filePath), { recursive: true });
|
|
2190
|
+
await fs2.writeFile(filePath, buf);
|
|
2191
2191
|
publicUrl = `/${opts.localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
2192
2192
|
}
|
|
2193
2193
|
await repo.save(
|
|
@@ -2609,12 +2609,12 @@ function createUploadHandler(config) {
|
|
|
2609
2609
|
const fileUrl = await storageService.upload(buffer, `uploads/${relativeUnderUploads}`, contentType);
|
|
2610
2610
|
return json({ filePath: fileUrl, parentId });
|
|
2611
2611
|
}
|
|
2612
|
-
const
|
|
2613
|
-
const
|
|
2614
|
-
const dir =
|
|
2615
|
-
const filePath =
|
|
2616
|
-
await
|
|
2617
|
-
await
|
|
2612
|
+
const fs2 = await import("fs/promises");
|
|
2613
|
+
const path2 = await import("path");
|
|
2614
|
+
const dir = path2.join(process.cwd(), localUploadDir);
|
|
2615
|
+
const filePath = path2.join(dir, relativeUnderUploads);
|
|
2616
|
+
await fs2.mkdir(path2.dirname(filePath), { recursive: true });
|
|
2617
|
+
await fs2.writeFile(filePath, buffer);
|
|
2618
2618
|
const urlRel = `${localUploadDir.replace(/^\/+/, "").replace(/\\/g, "/")}/${relativeUnderUploads.replace(/\\/g, "/")}`;
|
|
2619
2619
|
return json({ filePath: `/${urlRel}`, parentId });
|
|
2620
2620
|
} catch (err) {
|
|
@@ -3268,11 +3268,11 @@ function createUserAvatarHandler(config) {
|
|
|
3268
3268
|
const ext = file.name.split(".").pop() || "jpg";
|
|
3269
3269
|
const fileName = `avatar_${session.user.email}_${Date.now()}.${ext}`;
|
|
3270
3270
|
const avatarUrl = saveAvatar ? await saveAvatar(buffer, fileName) : await (async () => {
|
|
3271
|
-
const
|
|
3272
|
-
const
|
|
3273
|
-
const dir =
|
|
3274
|
-
await
|
|
3275
|
-
await
|
|
3271
|
+
const fs2 = await import("fs/promises");
|
|
3272
|
+
const path2 = await import("path");
|
|
3273
|
+
const dir = path2.join(process.cwd(), "public", "uploads", "avatars");
|
|
3274
|
+
await fs2.mkdir(dir, { recursive: true });
|
|
3275
|
+
await fs2.writeFile(path2.join(dir, fileName), buffer);
|
|
3276
3276
|
return `/uploads/avatars/${fileName}`;
|
|
3277
3277
|
})();
|
|
3278
3278
|
return json({ message: "Avatar uploaded successfully", avatarUrl });
|
|
@@ -5168,6 +5168,7 @@ var Blog = class {
|
|
|
5168
5168
|
id;
|
|
5169
5169
|
title;
|
|
5170
5170
|
content;
|
|
5171
|
+
socialMediaContent;
|
|
5171
5172
|
coverImage;
|
|
5172
5173
|
authorId;
|
|
5173
5174
|
categoryId;
|
|
@@ -5196,6 +5197,9 @@ __decorateClass([
|
|
|
5196
5197
|
__decorateClass([
|
|
5197
5198
|
Column11("text")
|
|
5198
5199
|
], Blog.prototype, "content", 2);
|
|
5200
|
+
__decorateClass([
|
|
5201
|
+
Column11("text", { nullable: true })
|
|
5202
|
+
], Blog.prototype, "socialMediaContent", 2);
|
|
5199
5203
|
__decorateClass([
|
|
5200
5204
|
Column11("varchar", { nullable: true })
|
|
5201
5205
|
], Blog.prototype, "coverImage", 2);
|
|
@@ -5885,7 +5889,7 @@ __decorateClass([
|
|
|
5885
5889
|
JoinColumn12({ name: "userId" })
|
|
5886
5890
|
], Contact.prototype, "user", 2);
|
|
5887
5891
|
__decorateClass([
|
|
5888
|
-
OneToMany8(() => FormSubmission, (
|
|
5892
|
+
OneToMany8(() => FormSubmission, (fs2) => fs2.contact)
|
|
5889
5893
|
], Contact.prototype, "form_submissions", 2);
|
|
5890
5894
|
__decorateClass([
|
|
5891
5895
|
OneToMany8(() => Address, (a) => a.contact)
|
|
@@ -7181,6 +7185,145 @@ LlmAgentKnowledgeDocument = __decorateClass([
|
|
|
7181
7185
|
Index2("IDX_llm_agent_knowledge_agent", ["agentId"])
|
|
7182
7186
|
], LlmAgentKnowledgeDocument);
|
|
7183
7187
|
|
|
7188
|
+
// src/entities/rss-feed.entity.ts
|
|
7189
|
+
import {
|
|
7190
|
+
Entity as Entity42,
|
|
7191
|
+
PrimaryGeneratedColumn as PrimaryGeneratedColumn42,
|
|
7192
|
+
Column as Column42,
|
|
7193
|
+
CreateDateColumn as CreateDateColumn2,
|
|
7194
|
+
UpdateDateColumn,
|
|
7195
|
+
OneToMany as OneToMany17
|
|
7196
|
+
} from "typeorm";
|
|
7197
|
+
|
|
7198
|
+
// src/entities/rss-article.entity.ts
|
|
7199
|
+
import {
|
|
7200
|
+
Entity as Entity41,
|
|
7201
|
+
PrimaryGeneratedColumn as PrimaryGeneratedColumn41,
|
|
7202
|
+
Column as Column41,
|
|
7203
|
+
ManyToOne as ManyToOne28,
|
|
7204
|
+
JoinColumn as JoinColumn28,
|
|
7205
|
+
CreateDateColumn,
|
|
7206
|
+
Unique as Unique3
|
|
7207
|
+
} from "typeorm";
|
|
7208
|
+
var RssArticle = class {
|
|
7209
|
+
id;
|
|
7210
|
+
rssFeedId;
|
|
7211
|
+
rssFeed;
|
|
7212
|
+
externalId;
|
|
7213
|
+
title;
|
|
7214
|
+
articleUrl;
|
|
7215
|
+
summary;
|
|
7216
|
+
content;
|
|
7217
|
+
author;
|
|
7218
|
+
publishedAt;
|
|
7219
|
+
imageUrl;
|
|
7220
|
+
rawData;
|
|
7221
|
+
contentHash;
|
|
7222
|
+
isProcessed;
|
|
7223
|
+
createdAt;
|
|
7224
|
+
};
|
|
7225
|
+
__decorateClass([
|
|
7226
|
+
PrimaryGeneratedColumn41("uuid")
|
|
7227
|
+
], RssArticle.prototype, "id", 2);
|
|
7228
|
+
__decorateClass([
|
|
7229
|
+
Column41("uuid")
|
|
7230
|
+
], RssArticle.prototype, "rssFeedId", 2);
|
|
7231
|
+
__decorateClass([
|
|
7232
|
+
ManyToOne28(() => RssFeed, (f) => f.articles, { onDelete: "CASCADE" }),
|
|
7233
|
+
JoinColumn28({ name: "rssFeedId" })
|
|
7234
|
+
], RssArticle.prototype, "rssFeed", 2);
|
|
7235
|
+
__decorateClass([
|
|
7236
|
+
Column41({ type: "text", nullable: true })
|
|
7237
|
+
], RssArticle.prototype, "externalId", 2);
|
|
7238
|
+
__decorateClass([
|
|
7239
|
+
Column41({ type: "text" })
|
|
7240
|
+
], RssArticle.prototype, "title", 2);
|
|
7241
|
+
__decorateClass([
|
|
7242
|
+
Column41({ type: "text" })
|
|
7243
|
+
], RssArticle.prototype, "articleUrl", 2);
|
|
7244
|
+
__decorateClass([
|
|
7245
|
+
Column41({ type: "text", nullable: true })
|
|
7246
|
+
], RssArticle.prototype, "summary", 2);
|
|
7247
|
+
__decorateClass([
|
|
7248
|
+
Column41({ type: "text", nullable: true })
|
|
7249
|
+
], RssArticle.prototype, "content", 2);
|
|
7250
|
+
__decorateClass([
|
|
7251
|
+
Column41({ type: "text", nullable: true })
|
|
7252
|
+
], RssArticle.prototype, "author", 2);
|
|
7253
|
+
__decorateClass([
|
|
7254
|
+
Column41({ type: "timestamp", nullable: true })
|
|
7255
|
+
], RssArticle.prototype, "publishedAt", 2);
|
|
7256
|
+
__decorateClass([
|
|
7257
|
+
Column41({ type: "text", nullable: true })
|
|
7258
|
+
], RssArticle.prototype, "imageUrl", 2);
|
|
7259
|
+
__decorateClass([
|
|
7260
|
+
Column41({ type: "jsonb", nullable: true })
|
|
7261
|
+
], RssArticle.prototype, "rawData", 2);
|
|
7262
|
+
__decorateClass([
|
|
7263
|
+
Column41({ type: "text", nullable: true })
|
|
7264
|
+
], RssArticle.prototype, "contentHash", 2);
|
|
7265
|
+
__decorateClass([
|
|
7266
|
+
Column41({ type: "boolean", default: false })
|
|
7267
|
+
], RssArticle.prototype, "isProcessed", 2);
|
|
7268
|
+
__decorateClass([
|
|
7269
|
+
CreateDateColumn()
|
|
7270
|
+
], RssArticle.prototype, "createdAt", 2);
|
|
7271
|
+
RssArticle = __decorateClass([
|
|
7272
|
+
Entity41("rss_articles"),
|
|
7273
|
+
Unique3("UQ_rss_articles_feed_article_url", ["rssFeedId", "articleUrl"])
|
|
7274
|
+
], RssArticle);
|
|
7275
|
+
|
|
7276
|
+
// src/entities/rss-feed.entity.ts
|
|
7277
|
+
var RssFeed = class {
|
|
7278
|
+
id;
|
|
7279
|
+
name;
|
|
7280
|
+
rssUrl;
|
|
7281
|
+
websiteUrl;
|
|
7282
|
+
isActive;
|
|
7283
|
+
fetchFrequencyMinutes;
|
|
7284
|
+
lastFetchedAt;
|
|
7285
|
+
lastArticleDate;
|
|
7286
|
+
articles;
|
|
7287
|
+
createdAt;
|
|
7288
|
+
updatedAt;
|
|
7289
|
+
};
|
|
7290
|
+
__decorateClass([
|
|
7291
|
+
PrimaryGeneratedColumn42("uuid")
|
|
7292
|
+
], RssFeed.prototype, "id", 2);
|
|
7293
|
+
__decorateClass([
|
|
7294
|
+
Column42({ type: "text" })
|
|
7295
|
+
], RssFeed.prototype, "name", 2);
|
|
7296
|
+
__decorateClass([
|
|
7297
|
+
Column42({ type: "text", unique: true })
|
|
7298
|
+
], RssFeed.prototype, "rssUrl", 2);
|
|
7299
|
+
__decorateClass([
|
|
7300
|
+
Column42({ type: "text", nullable: true })
|
|
7301
|
+
], RssFeed.prototype, "websiteUrl", 2);
|
|
7302
|
+
__decorateClass([
|
|
7303
|
+
Column42({ type: "boolean", default: true })
|
|
7304
|
+
], RssFeed.prototype, "isActive", 2);
|
|
7305
|
+
__decorateClass([
|
|
7306
|
+
Column42({ type: "int", default: 60 })
|
|
7307
|
+
], RssFeed.prototype, "fetchFrequencyMinutes", 2);
|
|
7308
|
+
__decorateClass([
|
|
7309
|
+
Column42({ type: "timestamp", nullable: true })
|
|
7310
|
+
], RssFeed.prototype, "lastFetchedAt", 2);
|
|
7311
|
+
__decorateClass([
|
|
7312
|
+
Column42({ type: "timestamp", nullable: true })
|
|
7313
|
+
], RssFeed.prototype, "lastArticleDate", 2);
|
|
7314
|
+
__decorateClass([
|
|
7315
|
+
OneToMany17(() => RssArticle, (article) => article.rssFeed)
|
|
7316
|
+
], RssFeed.prototype, "articles", 2);
|
|
7317
|
+
__decorateClass([
|
|
7318
|
+
CreateDateColumn2()
|
|
7319
|
+
], RssFeed.prototype, "createdAt", 2);
|
|
7320
|
+
__decorateClass([
|
|
7321
|
+
UpdateDateColumn()
|
|
7322
|
+
], RssFeed.prototype, "updatedAt", 2);
|
|
7323
|
+
RssFeed = __decorateClass([
|
|
7324
|
+
Entity42("rss_feeds")
|
|
7325
|
+
], RssFeed);
|
|
7326
|
+
|
|
7184
7327
|
// src/entities/index.ts
|
|
7185
7328
|
var CMS_ENTITY_MAP = {
|
|
7186
7329
|
users: User,
|
|
@@ -7222,9 +7365,1211 @@ var CMS_ENTITY_MAP = {
|
|
|
7222
7365
|
wishlists: Wishlist,
|
|
7223
7366
|
wishlist_items: WishlistItem,
|
|
7224
7367
|
llm_agents: LlmAgent,
|
|
7225
|
-
llm_agent_knowledge_documents: LlmAgentKnowledgeDocument
|
|
7368
|
+
llm_agent_knowledge_documents: LlmAgentKnowledgeDocument,
|
|
7369
|
+
rss_feeds: RssFeed,
|
|
7370
|
+
rss_articles: RssArticle
|
|
7226
7371
|
};
|
|
7227
7372
|
|
|
7373
|
+
// src/plugins/blog-generator/blog-generator-service.ts
|
|
7374
|
+
import Parser from "rss-parser";
|
|
7375
|
+
|
|
7376
|
+
// src/plugins/blog-generator/blog-generator-agent-defaults.ts
|
|
7377
|
+
var BLOG_GENERATOR_LLM_AGENT_SLUG = "blog-generator";
|
|
7378
|
+
var BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR = "---BLOG_GENERATOR_NEXT---";
|
|
7379
|
+
var BLOG_GENERATOR_DEFAULT_SYSTEM_INSTRUCTION = `You are a professional financial and business content writer.
|
|
7380
|
+
|
|
7381
|
+
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.
|
|
7382
|
+
|
|
7383
|
+
IMPORTANT RULES:
|
|
7384
|
+
|
|
7385
|
+
1. NEVER copy the RSS article directly.
|
|
7386
|
+
2. Rewrite the information into a fresh, human-like article.
|
|
7387
|
+
3. Expand the topic with professional insights and explanations.
|
|
7388
|
+
4. Maintain a natural editorial tone.
|
|
7389
|
+
5. Make the article SEO-friendly.
|
|
7390
|
+
6. Use engaging headings and subheadings.
|
|
7391
|
+
7. Avoid robotic AI phrasing.
|
|
7392
|
+
8. Do not mention that the content came from RSS or feeds.
|
|
7393
|
+
9. Preserve factual accuracy from the source material.
|
|
7394
|
+
10. The output should feel like a professionally written industry blog.
|
|
7395
|
+
|
|
7396
|
+
MULTIPLE FEEDS:
|
|
7397
|
+
- If several feeds are provided, compare them: if they largely cover the same story or overlap heavily, produce ONE cohesive article.
|
|
7398
|
+
- If they cover clearly different topics or distinct stories, produce MULTIPLE articles (one per distinct story).
|
|
7399
|
+
- You may also produce one synthesis plus a short separate angle when that best serves the reader\u2014use your judgment.
|
|
7400
|
+
|
|
7401
|
+
WRITING STYLE:
|
|
7402
|
+
- Professional
|
|
7403
|
+
- Clear
|
|
7404
|
+
- Informative
|
|
7405
|
+
- Business-focused
|
|
7406
|
+
- Human sounding
|
|
7407
|
+
- Modern editorial style
|
|
7408
|
+
|
|
7409
|
+
ARTICLE STRUCTURE (each article \u2014 express in HTML):
|
|
7410
|
+
1. Engaging introduction
|
|
7411
|
+
2. Industry context
|
|
7412
|
+
3. Main developments
|
|
7413
|
+
4. Key implications
|
|
7414
|
+
5. Expert/business analysis
|
|
7415
|
+
6. Conclusion
|
|
7416
|
+
|
|
7417
|
+
HTML STRUCTURE (STRICT \u2014 each article must be valid, semantic HTML only):
|
|
7418
|
+
- Output HTML only. Do not use Markdown (no # headings, no **bold**, no \`code fences\`).
|
|
7419
|
+
- Wrap each complete article in a single root: <article class="blog-post"> ... </article>
|
|
7420
|
+
- Inside <article>, use this outline:
|
|
7421
|
+
- <header><h1 class="blog-post-title">\u2026main title\u2026</h1></header> (exactly one h1 per article)
|
|
7422
|
+
- <section class="blog-post-body"> for all following content
|
|
7423
|
+
- Use <h2> and <h3> for section and subsection titles (never skip levels: h1 \u2192 h2 \u2192 h3).
|
|
7424
|
+
- Use <p> for paragraphs; keep paragraphs focused (avoid huge unbroken text).
|
|
7425
|
+
- Use <ul>/<ol> with <li> for lists where appropriate.
|
|
7426
|
+
- Use <strong> and <em> for emphasis sparingly; use <blockquote> only when quoting or callouts fit.
|
|
7427
|
+
- Do not include <html>, <head>, <body>, or document-level wrappers \u2014 only the fragment(s) described above.
|
|
7428
|
+
- Do not use <script>, <style>, <iframe>, or inline event handlers. Avoid inline style="" except when essential for accessibility (prefer none).
|
|
7429
|
+
- Escape angle brackets in body text if you must mention markup; keep output safe for embedding in a CMS.
|
|
7430
|
+
|
|
7431
|
+
CONTENT REQUIREMENTS:
|
|
7432
|
+
- Minimum 800 words per article unless source material is very small.
|
|
7433
|
+
- Add context around industry trends where appropriate.
|
|
7434
|
+
- Explain why the topic matters.
|
|
7435
|
+
- Include practical implications for businesses/professionals.
|
|
7436
|
+
|
|
7437
|
+
IF RSS CONTENT IS LIMITED:
|
|
7438
|
+
- Expand intelligently using general industry knowledge.
|
|
7439
|
+
- Keep the article relevant to the original topic.
|
|
7440
|
+
- Do not invent fake facts or statistics.
|
|
7441
|
+
|
|
7442
|
+
OUTPUT FORMAT (STRICT \u2014 HTML only):
|
|
7443
|
+
- Return ONLY the article HTML fragment(s). No JSON, no preamble or postscript ("Here is your article\u2026"), no markdown.
|
|
7444
|
+
- 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):
|
|
7445
|
+
${BLOG_GENERATOR_MARKDOWN_ARTICLE_SEPARATOR}
|
|
7446
|
+
- If you output a single article, use one <article> only and do not use that separator line.`;
|
|
7447
|
+
var BLOG_GENERATOR_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7448
|
+
{
|
|
7449
|
+
maxUserChars: 5e5,
|
|
7450
|
+
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.`
|
|
7451
|
+
},
|
|
7452
|
+
null,
|
|
7453
|
+
2
|
|
7454
|
+
);
|
|
7455
|
+
|
|
7456
|
+
// src/plugins/blog-generator/blog-generator-metadata-defaults.ts
|
|
7457
|
+
var BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG = "blog-generator-metadata";
|
|
7458
|
+
var BLOG_METADATA_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7459
|
+
{
|
|
7460
|
+
maxUserChars: 5e5,
|
|
7461
|
+
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)."
|
|
7462
|
+
},
|
|
7463
|
+
null,
|
|
7464
|
+
2
|
|
7465
|
+
);
|
|
7466
|
+
|
|
7467
|
+
// src/plugins/blog-generator/blog-generator-social-defaults.ts
|
|
7468
|
+
var BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG = "blog-generator-social";
|
|
7469
|
+
var BLOG_SOCIAL_ENRICHER_DEFAULT_VALIDATION_RULES = JSON.stringify(
|
|
7470
|
+
{
|
|
7471
|
+
maxUserChars: 5e5,
|
|
7472
|
+
guardrails: "Reply with one valid JSON object only. Key: socialMediaContent (string, plain text, max ~2900 chars). No markdown fences."
|
|
7473
|
+
},
|
|
7474
|
+
null,
|
|
7475
|
+
2
|
|
7476
|
+
);
|
|
7477
|
+
|
|
7478
|
+
// src/plugins/blog-generator/blog-generator-service.ts
|
|
7479
|
+
var parser = new Parser({
|
|
7480
|
+
defaultRSS: 2
|
|
7481
|
+
});
|
|
7482
|
+
function resolveBlogCategoryIdByName(needle, rows) {
|
|
7483
|
+
if (!needle?.trim()) return { categoryId: null, matched: false };
|
|
7484
|
+
const n = needle.trim().toLowerCase();
|
|
7485
|
+
const hit = rows.find((r) => r.name.trim().toLowerCase() === n);
|
|
7486
|
+
return hit ? { categoryId: hit.id, matched: true } : { categoryId: null, matched: false };
|
|
7487
|
+
}
|
|
7488
|
+
|
|
7489
|
+
// src/plugins/blog-generator/blog-generator-persist.ts
|
|
7490
|
+
function slugBase(draft) {
|
|
7491
|
+
const raw = (draft.slug?.trim() || draft.title || "post").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
7492
|
+
return raw || "post";
|
|
7493
|
+
}
|
|
7494
|
+
function normalizeSlugSegment(input) {
|
|
7495
|
+
const t = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 100);
|
|
7496
|
+
return t || "post";
|
|
7497
|
+
}
|
|
7498
|
+
async function allocateUniqueSlug(repo, base) {
|
|
7499
|
+
let b = normalizeSlugSegment(base);
|
|
7500
|
+
for (let i = 0; i < 120; i++) {
|
|
7501
|
+
const s = i === 0 ? b : `${b}-${i + 1}`.slice(0, 160);
|
|
7502
|
+
const exists = await repo.findOne({ where: { slug: s } });
|
|
7503
|
+
if (!exists) return s;
|
|
7504
|
+
}
|
|
7505
|
+
throw new Error("Could not allocate a unique slug");
|
|
7506
|
+
}
|
|
7507
|
+
async function findOrCreateTag(repo, name) {
|
|
7508
|
+
const trimmed = name.trim().slice(0, 500);
|
|
7509
|
+
if (!trimmed) throw new Error("Empty tag name");
|
|
7510
|
+
let row = await repo.findOne({ where: { name: trimmed } });
|
|
7511
|
+
if (row && row.deleted) {
|
|
7512
|
+
await repo.update(
|
|
7513
|
+
{ id: row.id },
|
|
7514
|
+
{ deleted: false, updatedAt: /* @__PURE__ */ new Date() }
|
|
7515
|
+
);
|
|
7516
|
+
row = await repo.findOne({ where: { id: row.id } });
|
|
7517
|
+
return row;
|
|
7518
|
+
}
|
|
7519
|
+
if (row) return row;
|
|
7520
|
+
const now = /* @__PURE__ */ new Date();
|
|
7521
|
+
return repo.save(
|
|
7522
|
+
repo.create({
|
|
7523
|
+
name: trimmed,
|
|
7524
|
+
deleted: false,
|
|
7525
|
+
createdAt: now,
|
|
7526
|
+
updatedAt: now
|
|
7527
|
+
})
|
|
7528
|
+
);
|
|
7529
|
+
}
|
|
7530
|
+
async function findOrCreateCategory(repo, displayName) {
|
|
7531
|
+
const n = displayName.trim().slice(0, 500);
|
|
7532
|
+
if (!n) return null;
|
|
7533
|
+
let row = await repo.createQueryBuilder("c").where("LOWER(TRIM(c.name)) = LOWER(TRIM(:n))", { n }).getOne();
|
|
7534
|
+
if (row && row.deleted) {
|
|
7535
|
+
await repo.update(
|
|
7536
|
+
{ id: row.id },
|
|
7537
|
+
{ deleted: false, updatedAt: /* @__PURE__ */ new Date() }
|
|
7538
|
+
);
|
|
7539
|
+
row = await repo.findOne({ where: { id: row.id } });
|
|
7540
|
+
return row;
|
|
7541
|
+
}
|
|
7542
|
+
if (row) return row;
|
|
7543
|
+
const now = /* @__PURE__ */ new Date();
|
|
7544
|
+
return repo.save(
|
|
7545
|
+
repo.create({
|
|
7546
|
+
name: n,
|
|
7547
|
+
deleted: false,
|
|
7548
|
+
createdAt: now,
|
|
7549
|
+
updatedAt: now
|
|
7550
|
+
})
|
|
7551
|
+
);
|
|
7552
|
+
}
|
|
7553
|
+
async function persistGeneratedBlogDraft(manager, maps, params) {
|
|
7554
|
+
const { draft, authorId } = params;
|
|
7555
|
+
const blogRepo = manager.getRepository(maps.blogs);
|
|
7556
|
+
const tagRepo = manager.getRepository(maps.tags);
|
|
7557
|
+
const catRepo = manager.getRepository(maps.categories);
|
|
7558
|
+
const seoRepo = manager.getRepository(maps.seos);
|
|
7559
|
+
const uniqueBlogSlug = await allocateUniqueSlug(blogRepo, slugBase(draft));
|
|
7560
|
+
const uniqueSeoSlug = await allocateUniqueSlug(seoRepo, uniqueBlogSlug);
|
|
7561
|
+
const seoRow = await seoRepo.save(
|
|
7562
|
+
seoRepo.create({
|
|
7563
|
+
slug: uniqueSeoSlug,
|
|
7564
|
+
title: draft.seo.title,
|
|
7565
|
+
description: draft.seo.description,
|
|
7566
|
+
keywords: draft.seo.keywords,
|
|
7567
|
+
ogTitle: draft.seo.ogTitle ?? draft.seo.title,
|
|
7568
|
+
ogDescription: draft.seo.ogDescription ?? draft.seo.description,
|
|
7569
|
+
ogImage: null,
|
|
7570
|
+
deleted: false,
|
|
7571
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
7572
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7573
|
+
})
|
|
7574
|
+
);
|
|
7575
|
+
const seoId = Number(seoRow.id);
|
|
7576
|
+
let categoryId = null;
|
|
7577
|
+
if (draft.categoryName?.trim()) {
|
|
7578
|
+
const cat = await findOrCreateCategory(catRepo, draft.categoryName);
|
|
7579
|
+
if (cat) categoryId = Number(cat.id);
|
|
7580
|
+
}
|
|
7581
|
+
const tagEntities = [];
|
|
7582
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7583
|
+
for (const t of draft.tags ?? []) {
|
|
7584
|
+
const key = t.trim().toLowerCase();
|
|
7585
|
+
if (!key || seen.has(key)) continue;
|
|
7586
|
+
seen.add(key);
|
|
7587
|
+
tagEntities.push(await findOrCreateTag(tagRepo, t));
|
|
7588
|
+
}
|
|
7589
|
+
const socialTrim = typeof draft.socialMediaContent === "string" && draft.socialMediaContent.trim() ? draft.socialMediaContent.trim().slice(0, 2900) : null;
|
|
7590
|
+
const now = /* @__PURE__ */ new Date();
|
|
7591
|
+
const blogRow = await blogRepo.save(
|
|
7592
|
+
blogRepo.create({
|
|
7593
|
+
title: draft.title.trim().slice(0, 500) || "Untitled",
|
|
7594
|
+
content: draft.markdown,
|
|
7595
|
+
socialMediaContent: socialTrim,
|
|
7596
|
+
slug: uniqueBlogSlug,
|
|
7597
|
+
authorId,
|
|
7598
|
+
categoryId,
|
|
7599
|
+
seoId,
|
|
7600
|
+
published: false,
|
|
7601
|
+
deleted: false,
|
|
7602
|
+
coverImage: null,
|
|
7603
|
+
createdBy: authorId,
|
|
7604
|
+
updatedBy: authorId,
|
|
7605
|
+
createdAt: now,
|
|
7606
|
+
updatedAt: now
|
|
7607
|
+
})
|
|
7608
|
+
);
|
|
7609
|
+
const blogId = Number(blogRow.id);
|
|
7610
|
+
if (tagEntities.length > 0) {
|
|
7611
|
+
const blog = await blogRepo.findOne({
|
|
7612
|
+
where: { id: blogId },
|
|
7613
|
+
relations: ["tags"]
|
|
7614
|
+
});
|
|
7615
|
+
if (blog) {
|
|
7616
|
+
blog.tags = tagEntities;
|
|
7617
|
+
await blogRepo.save(blog);
|
|
7618
|
+
}
|
|
7619
|
+
}
|
|
7620
|
+
const tagIds = tagEntities.map((e) => Number(e.id)).filter((id) => Number.isFinite(id));
|
|
7621
|
+
return { blogId, slug: uniqueBlogSlug, categoryId, seoId, tagIds };
|
|
7622
|
+
}
|
|
7623
|
+
async function persistAllGeneratedBlogDrafts(dataSource, maps, params) {
|
|
7624
|
+
const out = [];
|
|
7625
|
+
for (const draft of params.drafts) {
|
|
7626
|
+
const row = await dataSource.transaction(
|
|
7627
|
+
(manager) => persistGeneratedBlogDraft(manager, maps, { draft, authorId: params.authorId })
|
|
7628
|
+
);
|
|
7629
|
+
out.push(row);
|
|
7630
|
+
}
|
|
7631
|
+
return out;
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
// src/api/rss-feed-blog-api.ts
|
|
7635
|
+
function deriveRssFeedDisplayName(rssUrl) {
|
|
7636
|
+
try {
|
|
7637
|
+
const u = new URL(rssUrl);
|
|
7638
|
+
const h = u.hostname.replace(/^www\./i, "");
|
|
7639
|
+
return (h || rssUrl).slice(0, 500);
|
|
7640
|
+
} catch {
|
|
7641
|
+
return rssUrl.slice(0, 500);
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
async function upsertRssFeedRow(dataSource, feedEntity, rssUrl, opts) {
|
|
7645
|
+
const repo = dataSource.getRepository(feedEntity);
|
|
7646
|
+
const trimmed = rssUrl.trim();
|
|
7647
|
+
const name = (opts?.name?.trim() || deriveRssFeedDisplayName(trimmed)).slice(0, 500);
|
|
7648
|
+
let row = await repo.findOne({ where: { rssUrl: trimmed } });
|
|
7649
|
+
if (!row) {
|
|
7650
|
+
row = repo.create({
|
|
7651
|
+
name,
|
|
7652
|
+
rssUrl: trimmed,
|
|
7653
|
+
websiteUrl: opts?.websiteUrl?.trim() || null,
|
|
7654
|
+
isActive: true,
|
|
7655
|
+
fetchFrequencyMinutes: 60
|
|
7656
|
+
});
|
|
7657
|
+
} else {
|
|
7658
|
+
row.name = name;
|
|
7659
|
+
if (opts?.websiteUrl !== void 0) {
|
|
7660
|
+
row.websiteUrl = opts.websiteUrl?.trim() || null;
|
|
7661
|
+
}
|
|
7662
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
7663
|
+
}
|
|
7664
|
+
return repo.save(row);
|
|
7665
|
+
}
|
|
7666
|
+
async function recordRssFetchSuccess(dataSource, feedEntity, articleEntity, rssUrl, latest) {
|
|
7667
|
+
const feedRepo = dataSource.getRepository(feedEntity);
|
|
7668
|
+
const row = await feedRepo.findOne({ where: { rssUrl: rssUrl.trim() } });
|
|
7669
|
+
if (!row) return;
|
|
7670
|
+
row.lastFetchedAt = /* @__PURE__ */ new Date();
|
|
7671
|
+
if (latest?.date) {
|
|
7672
|
+
row.lastArticleDate = latest.date;
|
|
7673
|
+
}
|
|
7674
|
+
row.updatedAt = /* @__PURE__ */ new Date();
|
|
7675
|
+
await feedRepo.save(row);
|
|
7676
|
+
const articleUrl = latest?.link?.trim();
|
|
7677
|
+
if (!latest || !articleUrl || !articleEntity) return;
|
|
7678
|
+
const artRepo = dataSource.getRepository(articleEntity);
|
|
7679
|
+
await artRepo.upsert(
|
|
7680
|
+
{
|
|
7681
|
+
rssFeedId: row.id,
|
|
7682
|
+
externalId: articleUrl.slice(0, 2e3),
|
|
7683
|
+
title: (latest.title?.trim() || "Untitled").slice(0, 2e3),
|
|
7684
|
+
articleUrl: articleUrl.slice(0, 2e3),
|
|
7685
|
+
summary: latest.summary?.trim() ? latest.summary.trim().slice(0, 1e5) : null,
|
|
7686
|
+
content: latest.content?.trim() ? latest.content.trim().slice(0, 5e5) : null,
|
|
7687
|
+
author: latest.creator?.trim() ? latest.creator.trim().slice(0, 500) : null,
|
|
7688
|
+
publishedAt: latest.date,
|
|
7689
|
+
imageUrl: null,
|
|
7690
|
+
rawData: { source: "blog-generator-latest", rssUrl: rssUrl.trim() },
|
|
7691
|
+
contentHash: null,
|
|
7692
|
+
isProcessed: false
|
|
7693
|
+
},
|
|
7694
|
+
{ conflictPaths: ["rssFeedId", "articleUrl"], skipUpdateIfNoValuesChanged: false }
|
|
7695
|
+
);
|
|
7696
|
+
}
|
|
7697
|
+
function serializeRssFeed(f) {
|
|
7698
|
+
return {
|
|
7699
|
+
id: f.id,
|
|
7700
|
+
name: f.name,
|
|
7701
|
+
rssUrl: f.rssUrl,
|
|
7702
|
+
websiteUrl: f.websiteUrl,
|
|
7703
|
+
isActive: f.isActive,
|
|
7704
|
+
fetchFrequencyMinutes: f.fetchFrequencyMinutes,
|
|
7705
|
+
lastFetchedAt: f.lastFetchedAt?.toISOString() ?? null,
|
|
7706
|
+
lastArticleDate: f.lastArticleDate?.toISOString() ?? null,
|
|
7707
|
+
createdAt: f.createdAt.toISOString(),
|
|
7708
|
+
updatedAt: f.updatedAt.toISOString()
|
|
7709
|
+
};
|
|
7710
|
+
}
|
|
7711
|
+
|
|
7712
|
+
// src/plugins/social-media/social-media-api-handlers.ts
|
|
7713
|
+
import fs from "fs/promises";
|
|
7714
|
+
import path from "path";
|
|
7715
|
+
|
|
7716
|
+
// src/plugins/social-media/linkedin-client.ts
|
|
7717
|
+
var RESTLI = "2.0.0";
|
|
7718
|
+
async function linkedInGetUserinfo(accessToken) {
|
|
7719
|
+
const r = await fetch("https://api.linkedin.com/v2/userinfo", {
|
|
7720
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
7721
|
+
});
|
|
7722
|
+
const text = await r.text();
|
|
7723
|
+
if (!r.ok) {
|
|
7724
|
+
throw new Error(`LinkedIn userinfo failed (${r.status}): ${text.slice(0, 500)}`);
|
|
7725
|
+
}
|
|
7726
|
+
try {
|
|
7727
|
+
return JSON.parse(text);
|
|
7728
|
+
} catch {
|
|
7729
|
+
throw new Error("LinkedIn userinfo: invalid JSON");
|
|
7730
|
+
}
|
|
7731
|
+
}
|
|
7732
|
+
function personUrn(personSub) {
|
|
7733
|
+
const s = String(personSub || "").trim();
|
|
7734
|
+
if (s.startsWith("urn:li:person:")) return s;
|
|
7735
|
+
return `urn:li:person:${s}`;
|
|
7736
|
+
}
|
|
7737
|
+
async function linkedInRegisterImageUpload(accessToken, personSub) {
|
|
7738
|
+
const owner = personUrn(personSub);
|
|
7739
|
+
const r = await fetch("https://api.linkedin.com/v2/assets?action=registerUpload", {
|
|
7740
|
+
method: "POST",
|
|
7741
|
+
headers: {
|
|
7742
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7743
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7744
|
+
"Content-Type": "application/json"
|
|
7745
|
+
},
|
|
7746
|
+
body: JSON.stringify({
|
|
7747
|
+
registerUploadRequest: {
|
|
7748
|
+
recipes: ["urn:li:digitalmediaRecipe:feedshare-image"],
|
|
7749
|
+
owner,
|
|
7750
|
+
serviceRelationships: [
|
|
7751
|
+
{
|
|
7752
|
+
relationshipType: "OWNER",
|
|
7753
|
+
identifier: "urn:li:userGeneratedContent"
|
|
7754
|
+
}
|
|
7755
|
+
]
|
|
7756
|
+
}
|
|
7757
|
+
})
|
|
7758
|
+
});
|
|
7759
|
+
const text = await r.text();
|
|
7760
|
+
if (!r.ok) {
|
|
7761
|
+
throw new Error(`LinkedIn registerUpload failed (${r.status}): ${text.slice(0, 800)}`);
|
|
7762
|
+
}
|
|
7763
|
+
let data;
|
|
7764
|
+
try {
|
|
7765
|
+
data = JSON.parse(text);
|
|
7766
|
+
} catch {
|
|
7767
|
+
throw new Error("LinkedIn registerUpload: invalid JSON");
|
|
7768
|
+
}
|
|
7769
|
+
const v = data.value;
|
|
7770
|
+
const uploadUrl = v?.uploadMechanism?.["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]?.uploadUrl;
|
|
7771
|
+
const asset = v?.asset;
|
|
7772
|
+
if (!uploadUrl || !asset) {
|
|
7773
|
+
throw new Error("LinkedIn registerUpload: missing uploadUrl or asset");
|
|
7774
|
+
}
|
|
7775
|
+
return { uploadUrl, asset };
|
|
7776
|
+
}
|
|
7777
|
+
async function linkedInUploadBinary(accessToken, uploadUrl, body, contentType) {
|
|
7778
|
+
const r = await fetch(uploadUrl, {
|
|
7779
|
+
method: "PUT",
|
|
7780
|
+
headers: {
|
|
7781
|
+
Authorization: `Bearer ${accessToken}`,
|
|
7782
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7783
|
+
"Content-Type": contentType || "application/octet-stream"
|
|
7784
|
+
},
|
|
7785
|
+
body: new Uint8Array(body)
|
|
7786
|
+
});
|
|
7787
|
+
if (!r.ok) {
|
|
7788
|
+
const t = await r.text();
|
|
7789
|
+
throw new Error(`LinkedIn binary upload failed (${r.status}): ${t.slice(0, 500)}`);
|
|
7790
|
+
}
|
|
7791
|
+
}
|
|
7792
|
+
async function linkedInCreateImageShare(params) {
|
|
7793
|
+
const author = personUrn(params.personSub);
|
|
7794
|
+
const commentary = params.commentary.slice(0, 2900);
|
|
7795
|
+
const title = params.title.slice(0, 200);
|
|
7796
|
+
const description = (params.description ?? title).slice(0, 200);
|
|
7797
|
+
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7798
|
+
method: "POST",
|
|
7799
|
+
headers: {
|
|
7800
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
7801
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7802
|
+
"Content-Type": "application/json"
|
|
7803
|
+
},
|
|
7804
|
+
body: JSON.stringify({
|
|
7805
|
+
author,
|
|
7806
|
+
lifecycleState: "PUBLISHED",
|
|
7807
|
+
specificContent: {
|
|
7808
|
+
"com.linkedin.ugc.ShareContent": {
|
|
7809
|
+
shareCommentary: { attributes: [], text: commentary },
|
|
7810
|
+
shareMediaCategory: "IMAGE",
|
|
7811
|
+
media: [
|
|
7812
|
+
{
|
|
7813
|
+
status: "READY",
|
|
7814
|
+
description: { attributes: [], text: description },
|
|
7815
|
+
media: params.asset,
|
|
7816
|
+
title: { attributes: [], text: title }
|
|
7817
|
+
}
|
|
7818
|
+
]
|
|
7819
|
+
}
|
|
7820
|
+
},
|
|
7821
|
+
visibility: {
|
|
7822
|
+
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
|
7823
|
+
}
|
|
7824
|
+
})
|
|
7825
|
+
});
|
|
7826
|
+
const text = await r.text();
|
|
7827
|
+
if (!r.ok) {
|
|
7828
|
+
throw new Error(`LinkedIn ugcPosts failed (${r.status}): ${text.slice(0, 1200)}`);
|
|
7829
|
+
}
|
|
7830
|
+
let data;
|
|
7831
|
+
try {
|
|
7832
|
+
data = JSON.parse(text);
|
|
7833
|
+
} catch {
|
|
7834
|
+
return { status: r.status };
|
|
7835
|
+
}
|
|
7836
|
+
return { status: r.status, id: data.id };
|
|
7837
|
+
}
|
|
7838
|
+
async function linkedInCreateTextShare(params) {
|
|
7839
|
+
const author = personUrn(params.personSub);
|
|
7840
|
+
const commentary = params.commentary.slice(0, 2900);
|
|
7841
|
+
const r = await fetch("https://api.linkedin.com/v2/ugcPosts", {
|
|
7842
|
+
method: "POST",
|
|
7843
|
+
headers: {
|
|
7844
|
+
Authorization: `Bearer ${params.accessToken}`,
|
|
7845
|
+
"X-Restli-Protocol-Version": RESTLI,
|
|
7846
|
+
"Content-Type": "application/json"
|
|
7847
|
+
},
|
|
7848
|
+
body: JSON.stringify({
|
|
7849
|
+
author,
|
|
7850
|
+
lifecycleState: "PUBLISHED",
|
|
7851
|
+
specificContent: {
|
|
7852
|
+
"com.linkedin.ugc.ShareContent": {
|
|
7853
|
+
shareCommentary: { attributes: [], text: commentary },
|
|
7854
|
+
shareMediaCategory: "NONE"
|
|
7855
|
+
}
|
|
7856
|
+
},
|
|
7857
|
+
visibility: {
|
|
7858
|
+
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
|
7859
|
+
}
|
|
7860
|
+
})
|
|
7861
|
+
});
|
|
7862
|
+
const text = await r.text();
|
|
7863
|
+
if (!r.ok) {
|
|
7864
|
+
throw new Error(`LinkedIn ugcPosts (text) failed (${r.status}): ${text.slice(0, 1200)}`);
|
|
7865
|
+
}
|
|
7866
|
+
let data;
|
|
7867
|
+
try {
|
|
7868
|
+
data = JSON.parse(text);
|
|
7869
|
+
} catch {
|
|
7870
|
+
return { status: r.status };
|
|
7871
|
+
}
|
|
7872
|
+
return { status: r.status, id: data.id };
|
|
7873
|
+
}
|
|
7874
|
+
|
|
7875
|
+
// src/plugins/social-media/meta-service.ts
|
|
7876
|
+
var GRAPH_API_VERSION = "v23.0";
|
|
7877
|
+
var GRAPH_BASE = `https://graph.facebook.com/${GRAPH_API_VERSION}`;
|
|
7878
|
+
async function metaFetchUserManagedPages(userAccessToken) {
|
|
7879
|
+
const token = String(userAccessToken || "").trim();
|
|
7880
|
+
if (!token) {
|
|
7881
|
+
throw new Error("User access token is required");
|
|
7882
|
+
}
|
|
7883
|
+
const url = new URL(`${GRAPH_BASE}/me/accounts`);
|
|
7884
|
+
url.searchParams.set("access_token", token);
|
|
7885
|
+
const r = await fetch(url.toString(), { method: "GET", headers: { Accept: "application/json" } });
|
|
7886
|
+
const text = await r.text();
|
|
7887
|
+
let body;
|
|
7888
|
+
try {
|
|
7889
|
+
body = JSON.parse(text);
|
|
7890
|
+
} catch {
|
|
7891
|
+
throw new Error(`Facebook Graph returned invalid JSON (${r.status})`);
|
|
7892
|
+
}
|
|
7893
|
+
if (body.error?.message) {
|
|
7894
|
+
const code = body.error.code != null ? ` (code ${body.error.code})` : "";
|
|
7895
|
+
throw new Error(`${body.error.message}${code}`);
|
|
7896
|
+
}
|
|
7897
|
+
if (!r.ok) {
|
|
7898
|
+
throw new Error(`Facebook Graph request failed (${r.status}): ${text.slice(0, 600)}`);
|
|
7899
|
+
}
|
|
7900
|
+
return body;
|
|
7901
|
+
}
|
|
7902
|
+
function parseGraphMutation(text, httpOk) {
|
|
7903
|
+
let body;
|
|
7904
|
+
try {
|
|
7905
|
+
body = JSON.parse(text);
|
|
7906
|
+
} catch {
|
|
7907
|
+
throw new Error(`Facebook Graph returned invalid JSON (${httpOk ? "200" : "error"})`);
|
|
7908
|
+
}
|
|
7909
|
+
if (body.error?.message) {
|
|
7910
|
+
const code = body.error.code != null ? ` (code ${body.error.code})` : "";
|
|
7911
|
+
throw new Error(`${body.error.message}${code}`);
|
|
7912
|
+
}
|
|
7913
|
+
if (!httpOk) {
|
|
7914
|
+
throw new Error(`Facebook Graph request failed: ${text.slice(0, 600)}`);
|
|
7915
|
+
}
|
|
7916
|
+
return body;
|
|
7917
|
+
}
|
|
7918
|
+
function metaResolvePageAccessToken(accounts, pageId) {
|
|
7919
|
+
const id = String(pageId || "").trim();
|
|
7920
|
+
if (!id || !accounts.data?.length) return null;
|
|
7921
|
+
const row = accounts.data.find((p) => String(p.id ?? "").trim() === id);
|
|
7922
|
+
const tok = row?.access_token;
|
|
7923
|
+
return typeof tok === "string" && tok.trim() ? tok.trim() : null;
|
|
7924
|
+
}
|
|
7925
|
+
async function metaPostPagePhoto(opts) {
|
|
7926
|
+
const { pageId, pageAccessToken, imageBuffer, contentType, caption } = opts;
|
|
7927
|
+
const pid = String(pageId || "").trim();
|
|
7928
|
+
const tok = String(pageAccessToken || "").trim();
|
|
7929
|
+
if (!pid || !tok) throw new Error("Page ID and page access token are required");
|
|
7930
|
+
if (!imageBuffer?.length) throw new Error("Image buffer is empty");
|
|
7931
|
+
const url = `${GRAPH_BASE}/${encodeURIComponent(pid)}/photos`;
|
|
7932
|
+
const lower = contentType.split(";")[0].trim().toLowerCase();
|
|
7933
|
+
const filename = lower.includes("png") ? "photo.png" : "photo.jpg";
|
|
7934
|
+
const blob = new Blob([new Uint8Array(imageBuffer)], { type: lower || "image/jpeg" });
|
|
7935
|
+
const form = new FormData();
|
|
7936
|
+
form.append("access_token", tok);
|
|
7937
|
+
form.append("caption", String(caption || "").trim().slice(0, 2200));
|
|
7938
|
+
form.append("source", blob, filename);
|
|
7939
|
+
const r = await fetch(url, { method: "POST", body: form });
|
|
7940
|
+
const text = await r.text();
|
|
7941
|
+
return parseGraphMutation(text, r.ok);
|
|
7942
|
+
}
|
|
7943
|
+
async function metaPostPageFeed(opts) {
|
|
7944
|
+
const { pageId, pageAccessToken, message } = opts;
|
|
7945
|
+
const pid = String(pageId || "").trim();
|
|
7946
|
+
const tok = String(pageAccessToken || "").trim();
|
|
7947
|
+
if (!pid || !tok) throw new Error("Page ID and page access token are required");
|
|
7948
|
+
const url = new URL(`${GRAPH_BASE}/${encodeURIComponent(pid)}/feed`);
|
|
7949
|
+
const params = new URLSearchParams();
|
|
7950
|
+
params.set("access_token", tok);
|
|
7951
|
+
params.set("message", String(message || "").trim().slice(0, 5e3));
|
|
7952
|
+
const r = await fetch(url.toString(), {
|
|
7953
|
+
method: "POST",
|
|
7954
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
7955
|
+
body: params.toString()
|
|
7956
|
+
});
|
|
7957
|
+
const text = await r.text();
|
|
7958
|
+
return parseGraphMutation(text, r.ok);
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7961
|
+
// src/plugins/social-media/social-media-api-handlers.ts
|
|
7962
|
+
var SETTINGS_GROUP = "social_media";
|
|
7963
|
+
async function loadGroupMap(dataSource, entityMap, encryptionKey) {
|
|
7964
|
+
if (!entityMap.configs) return {};
|
|
7965
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
7966
|
+
const rows = await repo.find({ where: { settings: SETTINGS_GROUP, deleted: false } });
|
|
7967
|
+
const out = {};
|
|
7968
|
+
for (const row of rows) {
|
|
7969
|
+
let val = row.value;
|
|
7970
|
+
if (row.encrypted && encryptionKey) {
|
|
7971
|
+
try {
|
|
7972
|
+
val = simpleDecrypt(val, encryptionKey);
|
|
7973
|
+
} catch {
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
out[row.key] = val;
|
|
7977
|
+
}
|
|
7978
|
+
return out;
|
|
7979
|
+
}
|
|
7980
|
+
async function upsertConfigKey(dataSource, entityMap, key, value, opts) {
|
|
7981
|
+
if (!entityMap.configs) throw new Error("configs entity missing");
|
|
7982
|
+
const repo = dataSource.getRepository(entityMap.configs);
|
|
7983
|
+
let stored = value;
|
|
7984
|
+
if (opts.encrypted && opts.encryptionKey) {
|
|
7985
|
+
stored = simpleEncrypt(value, opts.encryptionKey);
|
|
7986
|
+
}
|
|
7987
|
+
const existing = await repo.findOne({ where: { settings: SETTINGS_GROUP, key } });
|
|
7988
|
+
if (existing) {
|
|
7989
|
+
await repo.update(existing.id, {
|
|
7990
|
+
value: stored,
|
|
7991
|
+
type: opts.type,
|
|
7992
|
+
encrypted: opts.encrypted,
|
|
7993
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
7994
|
+
});
|
|
7995
|
+
} else {
|
|
7996
|
+
await repo.save(
|
|
7997
|
+
repo.create({
|
|
7998
|
+
settings: SETTINGS_GROUP,
|
|
7999
|
+
key,
|
|
8000
|
+
value: stored,
|
|
8001
|
+
type: opts.type,
|
|
8002
|
+
encrypted: opts.encrypted
|
|
8003
|
+
})
|
|
8004
|
+
);
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
function stripHtml(html, maxLen) {
|
|
8008
|
+
const t = String(html || "").replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
8009
|
+
return t.slice(0, maxLen);
|
|
8010
|
+
}
|
|
8011
|
+
function decodeHtmlAttr(s) {
|
|
8012
|
+
return String(s).replace(/&/gi, "&").replace(/"/gi, '"').replace(/'/g, "'").replace(/</gi, "<").replace(/>/gi, ">");
|
|
8013
|
+
}
|
|
8014
|
+
function extractFirstImageSrcFromHtml(html) {
|
|
8015
|
+
const s = String(html);
|
|
8016
|
+
const patterns = [
|
|
8017
|
+
/<img[^>]*\bsrc\s*=\s*["']([^"']+)["']/i,
|
|
8018
|
+
/<img[^>]*\bsrc\s*=\s*([^\s>]+)/i,
|
|
8019
|
+
/<img[^>]*\bdata-src\s*=\s*["']([^"']+)["']/i,
|
|
8020
|
+
/<img[^>]*\bdata-src\s*=\s*([^\s>]+)/i
|
|
8021
|
+
];
|
|
8022
|
+
for (const re of patterns) {
|
|
8023
|
+
const m = s.match(re);
|
|
8024
|
+
const raw = m?.[1]?.trim();
|
|
8025
|
+
if (raw) return decodeHtmlAttr(raw);
|
|
8026
|
+
}
|
|
8027
|
+
return null;
|
|
8028
|
+
}
|
|
8029
|
+
function resolveAbsoluteUrl(publicSiteUrl, pathOrUrl) {
|
|
8030
|
+
if (!pathOrUrl || !String(pathOrUrl).trim()) return null;
|
|
8031
|
+
const s = decodeHtmlAttr(String(pathOrUrl).trim());
|
|
8032
|
+
if (/^https?:\/\//i.test(s)) return s;
|
|
8033
|
+
if (s.startsWith("//")) return `https:${s}`;
|
|
8034
|
+
const base = (publicSiteUrl || "").replace(/\/+$/, "");
|
|
8035
|
+
if (!base) return null;
|
|
8036
|
+
const rel = s.startsWith("/") ? s : `/${s}`;
|
|
8037
|
+
return `${base}${rel}`;
|
|
8038
|
+
}
|
|
8039
|
+
function guessImageContentType(filePathOrUrl, buf) {
|
|
8040
|
+
const lower = filePathOrUrl.toLowerCase();
|
|
8041
|
+
if (lower.includes(".png")) return "image/png";
|
|
8042
|
+
if (lower.includes(".webp")) return "image/webp";
|
|
8043
|
+
if (lower.includes(".gif")) return "image/gif";
|
|
8044
|
+
if (lower.includes(".jpg") || lower.includes(".jpeg")) return "image/jpeg";
|
|
8045
|
+
if (buf.length >= 2 && buf[0] === 255 && buf[1] === 216) return "image/jpeg";
|
|
8046
|
+
if (buf.length >= 8 && buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
8047
|
+
if (buf.length >= 12 && buf[8] === 87 && buf[9] === 69 && buf[10] === 66 && buf[11] === 80) return "image/webp";
|
|
8048
|
+
return "image/jpeg";
|
|
8049
|
+
}
|
|
8050
|
+
function isLinkedInFeedshareCompatibleContentType(mime) {
|
|
8051
|
+
const m = mime.split(";")[0].trim().toLowerCase();
|
|
8052
|
+
return m === "image/jpeg" || m === "image/png";
|
|
8053
|
+
}
|
|
8054
|
+
async function loadImageBytesForBlogShare(coverImage, contentHtml, publicSiteUrl, req) {
|
|
8055
|
+
const origin = publicSiteUrl || inferRequestOrigin(req) || "";
|
|
8056
|
+
const fromHtml = extractFirstImageSrcFromHtml(contentHtml);
|
|
8057
|
+
const candidates = [];
|
|
8058
|
+
const seenDecoded = /* @__PURE__ */ new Set();
|
|
8059
|
+
const pushCand = (role, raw) => {
|
|
8060
|
+
const decoded = decodeHtmlAttr(raw.trim());
|
|
8061
|
+
if (!decoded || decoded.startsWith("data:")) return;
|
|
8062
|
+
if (seenDecoded.has(decoded)) return;
|
|
8063
|
+
seenDecoded.add(decoded);
|
|
8064
|
+
candidates.push({ role, raw });
|
|
8065
|
+
};
|
|
8066
|
+
if (coverImage?.trim()) pushCand("cover", coverImage.trim());
|
|
8067
|
+
if (fromHtml) pushCand("bodyImage", fromHtml);
|
|
8068
|
+
for (const { role, raw } of candidates) {
|
|
8069
|
+
const decoded = decodeHtmlAttr(raw.trim());
|
|
8070
|
+
if (decoded.startsWith("data:")) continue;
|
|
8071
|
+
if (decoded.startsWith("/uploads/") || decoded.startsWith("uploads/")) {
|
|
8072
|
+
const rel = decoded.startsWith("/") ? decoded.slice(1) : decoded;
|
|
8073
|
+
const tries = [path.join(process.cwd(), "public", rel), path.join(process.cwd(), rel)];
|
|
8074
|
+
for (const filePath of tries) {
|
|
8075
|
+
try {
|
|
8076
|
+
const buf = await fs.readFile(filePath);
|
|
8077
|
+
if (buf.length > 0) {
|
|
8078
|
+
const contentType = guessImageContentType(filePath, buf);
|
|
8079
|
+
if (!isLinkedInFeedshareCompatibleContentType(contentType)) {
|
|
8080
|
+
continue;
|
|
8081
|
+
}
|
|
8082
|
+
return {
|
|
8083
|
+
buffer: buf,
|
|
8084
|
+
contentType,
|
|
8085
|
+
source: `${role}:local_file:${truncateForLog(rel, 120)}`
|
|
8086
|
+
};
|
|
8087
|
+
}
|
|
8088
|
+
} catch {
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
}
|
|
8092
|
+
const url = resolveAbsoluteUrl(origin, decoded);
|
|
8093
|
+
if (!url) continue;
|
|
8094
|
+
try {
|
|
8095
|
+
const imgRes = await fetch(url, {
|
|
8096
|
+
headers: {
|
|
8097
|
+
"User-Agent": "InfuroCMS/1.0 (social share; +https://infuro.com)",
|
|
8098
|
+
Accept: "image/*,*/*;q=0.5"
|
|
8099
|
+
}
|
|
8100
|
+
});
|
|
8101
|
+
if (!imgRes.ok) continue;
|
|
8102
|
+
const buf = Buffer.from(await imgRes.arrayBuffer());
|
|
8103
|
+
if (buf.length === 0) continue;
|
|
8104
|
+
const hdr = imgRes.headers.get("content-type");
|
|
8105
|
+
const contentType = hdr && /^image\//i.test(hdr) ? hdr.split(";")[0].trim() : guessImageContentType(url, buf);
|
|
8106
|
+
if (!isLinkedInFeedshareCompatibleContentType(contentType)) {
|
|
8107
|
+
continue;
|
|
8108
|
+
}
|
|
8109
|
+
return {
|
|
8110
|
+
buffer: buf,
|
|
8111
|
+
contentType,
|
|
8112
|
+
source: `${role}:fetch:${truncateForLog(url, 160)}`
|
|
8113
|
+
};
|
|
8114
|
+
} catch {
|
|
8115
|
+
}
|
|
8116
|
+
}
|
|
8117
|
+
return null;
|
|
8118
|
+
}
|
|
8119
|
+
function inferRequestOrigin(req) {
|
|
8120
|
+
const host = req.headers.get("x-forwarded-host") || req.headers.get("host");
|
|
8121
|
+
if (!host) return void 0;
|
|
8122
|
+
const rawProto = req.headers.get("x-forwarded-proto") || "https";
|
|
8123
|
+
const proto = rawProto.split(",")[0]?.trim() || "https";
|
|
8124
|
+
return `${proto}://${host}`.replace(/\/+$/, "");
|
|
8125
|
+
}
|
|
8126
|
+
function linkedInPublishLoggingEnabled(config) {
|
|
8127
|
+
if (config.linkedInPublishLogging === false) return false;
|
|
8128
|
+
if (config.linkedInPublishLogging === true) return true;
|
|
8129
|
+
try {
|
|
8130
|
+
if (typeof process !== "undefined" && String(process.env.CMS_LINKEDIN_PUBLISH_LOG).trim() === "0") {
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
} catch {
|
|
8134
|
+
}
|
|
8135
|
+
return true;
|
|
8136
|
+
}
|
|
8137
|
+
function logLinkedInPublish(enabled, event, detail) {
|
|
8138
|
+
if (!enabled) return;
|
|
8139
|
+
const line = `[LinkedIn publish] ${event} ${JSON.stringify(detail)}`;
|
|
8140
|
+
if (event === "failed" || event === "image_path_failed_falling_back_to_text") {
|
|
8141
|
+
console.warn(line);
|
|
8142
|
+
} else {
|
|
8143
|
+
console.info(line);
|
|
8144
|
+
}
|
|
8145
|
+
}
|
|
8146
|
+
function truncateForLog(s, max = 160) {
|
|
8147
|
+
const t = String(s).replace(/\s+/g, " ").trim();
|
|
8148
|
+
if (t.length <= max) return t;
|
|
8149
|
+
return `${t.slice(0, max)}\u2026`;
|
|
8150
|
+
}
|
|
8151
|
+
function createSocialMediaHandlers(config) {
|
|
8152
|
+
const {
|
|
8153
|
+
dataSource,
|
|
8154
|
+
entityMap,
|
|
8155
|
+
json,
|
|
8156
|
+
requireAuth,
|
|
8157
|
+
requireEntityPermission,
|
|
8158
|
+
encryptionKey,
|
|
8159
|
+
publicSiteUrl
|
|
8160
|
+
} = config;
|
|
8161
|
+
const publishLog = linkedInPublishLoggingEnabled(config);
|
|
8162
|
+
return {
|
|
8163
|
+
async getLinkedInStatus(req) {
|
|
8164
|
+
const a = await requireAuth(req);
|
|
8165
|
+
if (a) return a;
|
|
8166
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8167
|
+
if (pe) return pe;
|
|
8168
|
+
try {
|
|
8169
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8170
|
+
const enabled = map.enabled !== "false";
|
|
8171
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8172
|
+
const sub = (map.linkedin_person_sub ?? "").trim();
|
|
8173
|
+
return json({
|
|
8174
|
+
ok: true,
|
|
8175
|
+
linkedInReady: enabled && Boolean(token) && Boolean(sub),
|
|
8176
|
+
enabled,
|
|
8177
|
+
hasToken: Boolean(token),
|
|
8178
|
+
hasPersonSub: Boolean(sub)
|
|
8179
|
+
});
|
|
8180
|
+
} catch (e) {
|
|
8181
|
+
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8182
|
+
return json({ error: msg }, { status: 500 });
|
|
8183
|
+
}
|
|
8184
|
+
},
|
|
8185
|
+
async syncLinkedInProfile(req) {
|
|
8186
|
+
const a = await requireAuth(req);
|
|
8187
|
+
if (a) return a;
|
|
8188
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8189
|
+
if (pe) return pe;
|
|
8190
|
+
try {
|
|
8191
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8192
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8193
|
+
if (!token) {
|
|
8194
|
+
return json({ error: "Save a LinkedIn access token in Plugins \u2192 Social media first." }, { status: 400 });
|
|
8195
|
+
}
|
|
8196
|
+
const info = await linkedInGetUserinfo(token);
|
|
8197
|
+
const sub = String(info.sub ?? "").trim();
|
|
8198
|
+
if (!sub) {
|
|
8199
|
+
return json({ error: "LinkedIn userinfo did not return a subject (sub)." }, { status: 502 });
|
|
8200
|
+
}
|
|
8201
|
+
await upsertConfigKey(dataSource, entityMap, "linkedin_person_sub", sub, {
|
|
8202
|
+
type: "private",
|
|
8203
|
+
encrypted: false,
|
|
8204
|
+
encryptionKey
|
|
8205
|
+
});
|
|
8206
|
+
return json({
|
|
8207
|
+
ok: true,
|
|
8208
|
+
linkedin_person_sub: sub,
|
|
8209
|
+
name: info.name ?? null
|
|
8210
|
+
});
|
|
8211
|
+
} catch (e) {
|
|
8212
|
+
const msg = e instanceof Error ? e.message : "Sync failed";
|
|
8213
|
+
return json({ error: msg }, { status: 502 });
|
|
8214
|
+
}
|
|
8215
|
+
},
|
|
8216
|
+
async publishBlogToLinkedIn(req, blogId) {
|
|
8217
|
+
const a = await requireAuth(req);
|
|
8218
|
+
if (a) return a;
|
|
8219
|
+
const pe = await requireEntityPermission(req, "blogs", "update");
|
|
8220
|
+
if (pe) return pe;
|
|
8221
|
+
if (!Number.isFinite(blogId) || blogId < 1) {
|
|
8222
|
+
return json({ error: "Invalid blog id" }, { status: 400 });
|
|
8223
|
+
}
|
|
8224
|
+
if (!entityMap.blogs) {
|
|
8225
|
+
return json({ error: "Blogs entity not configured" }, { status: 503 });
|
|
8226
|
+
}
|
|
8227
|
+
try {
|
|
8228
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8229
|
+
if (map.enabled === "false") {
|
|
8230
|
+
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8231
|
+
}
|
|
8232
|
+
const token = (map.linkedin_access_token ?? "").trim();
|
|
8233
|
+
const personSub = (map.linkedin_person_sub ?? "").trim();
|
|
8234
|
+
if (!token || !personSub) {
|
|
8235
|
+
return json(
|
|
8236
|
+
{ error: "Configure LinkedIn access token and sync profile (Plugins \u2192 Social media)." },
|
|
8237
|
+
{ status: 400 }
|
|
8238
|
+
);
|
|
8239
|
+
}
|
|
8240
|
+
const blogRepo = dataSource.getRepository(entityMap.blogs);
|
|
8241
|
+
const blog = await blogRepo.findOne({
|
|
8242
|
+
where: { id: blogId, deleted: false }
|
|
8243
|
+
});
|
|
8244
|
+
if (!blog) {
|
|
8245
|
+
return json({ error: "Blog not found" }, { status: 404 });
|
|
8246
|
+
}
|
|
8247
|
+
const b = blog;
|
|
8248
|
+
const title = String(b.title ?? "Blog").trim() || "Blog";
|
|
8249
|
+
const social = String(b.socialMediaContent ?? "").trim();
|
|
8250
|
+
const excerpt = stripHtml(String(b.content ?? ""), 2800);
|
|
8251
|
+
const commentary = (social || `${title}
|
|
8252
|
+
|
|
8253
|
+
${excerpt}`).trim().slice(0, 2900);
|
|
8254
|
+
const commentaryFrom = social ? "socialMediaContent" : "titleAndExcerpt";
|
|
8255
|
+
logLinkedInPublish(publishLog, "start", {
|
|
8256
|
+
blogId,
|
|
8257
|
+
titleLen: title.length,
|
|
8258
|
+
commentaryFrom,
|
|
8259
|
+
commentaryChars: commentary.length,
|
|
8260
|
+
hasCover: Boolean(b.coverImage?.trim()),
|
|
8261
|
+
contentHtmlChars: String(b.content ?? "").length
|
|
8262
|
+
});
|
|
8263
|
+
const imagePayload = await loadImageBytesForBlogShare(
|
|
8264
|
+
b.coverImage,
|
|
8265
|
+
String(b.content ?? ""),
|
|
8266
|
+
publicSiteUrl || inferRequestOrigin(req),
|
|
8267
|
+
req
|
|
8268
|
+
);
|
|
8269
|
+
if (imagePayload) {
|
|
8270
|
+
logLinkedInPublish(publishLog, "image_bytes_ready", {
|
|
8271
|
+
blogId,
|
|
8272
|
+
source: imagePayload.source,
|
|
8273
|
+
bytes: imagePayload.buffer.length,
|
|
8274
|
+
contentType: imagePayload.contentType
|
|
8275
|
+
});
|
|
8276
|
+
try {
|
|
8277
|
+
const { uploadUrl, asset } = await linkedInRegisterImageUpload(token, personSub);
|
|
8278
|
+
logLinkedInPublish(publishLog, "image_registered", {
|
|
8279
|
+
blogId,
|
|
8280
|
+
asset: truncateForLog(asset, 120),
|
|
8281
|
+
uploadHost: (() => {
|
|
8282
|
+
try {
|
|
8283
|
+
return new URL(uploadUrl).host;
|
|
8284
|
+
} catch {
|
|
8285
|
+
return "unknown";
|
|
8286
|
+
}
|
|
8287
|
+
})()
|
|
8288
|
+
});
|
|
8289
|
+
await linkedInUploadBinary(token, uploadUrl, imagePayload.buffer, imagePayload.contentType);
|
|
8290
|
+
logLinkedInPublish(publishLog, "image_uploaded", { blogId, bytes: imagePayload.buffer.length });
|
|
8291
|
+
const out2 = await linkedInCreateImageShare({
|
|
8292
|
+
accessToken: token,
|
|
8293
|
+
personSub,
|
|
8294
|
+
asset,
|
|
8295
|
+
commentary,
|
|
8296
|
+
title,
|
|
8297
|
+
description: title
|
|
8298
|
+
});
|
|
8299
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8300
|
+
blogId,
|
|
8301
|
+
mode: "image_plus_text",
|
|
8302
|
+
linkedinPostId: out2.id ?? null,
|
|
8303
|
+
commentaryFrom,
|
|
8304
|
+
commentaryChars: commentary.length,
|
|
8305
|
+
imageSource: imagePayload.source
|
|
8306
|
+
});
|
|
8307
|
+
return json({
|
|
8308
|
+
ok: true,
|
|
8309
|
+
linkedin: out2,
|
|
8310
|
+
usedImage: true,
|
|
8311
|
+
publishSummary: {
|
|
8312
|
+
mode: "image_plus_text",
|
|
8313
|
+
commentaryFrom,
|
|
8314
|
+
commentaryChars: commentary.length,
|
|
8315
|
+
image: {
|
|
8316
|
+
bytes: imagePayload.buffer.length,
|
|
8317
|
+
contentType: imagePayload.contentType,
|
|
8318
|
+
source: imagePayload.source
|
|
8319
|
+
},
|
|
8320
|
+
linkedinPostId: out2.id ?? null
|
|
8321
|
+
}
|
|
8322
|
+
});
|
|
8323
|
+
} catch (imgErr) {
|
|
8324
|
+
const imgMsg = imgErr instanceof Error ? imgErr.message : String(imgErr);
|
|
8325
|
+
logLinkedInPublish(publishLog, "image_path_failed_falling_back_to_text", {
|
|
8326
|
+
blogId,
|
|
8327
|
+
message: truncateForLog(imgMsg, 500)
|
|
8328
|
+
});
|
|
8329
|
+
const out2 = await linkedInCreateTextShare({
|
|
8330
|
+
accessToken: token,
|
|
8331
|
+
personSub,
|
|
8332
|
+
commentary
|
|
8333
|
+
});
|
|
8334
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8335
|
+
blogId,
|
|
8336
|
+
mode: "text_only_after_image_failure",
|
|
8337
|
+
linkedinPostId: out2.id ?? null,
|
|
8338
|
+
commentaryFrom,
|
|
8339
|
+
commentaryChars: commentary.length,
|
|
8340
|
+
imageError: truncateForLog(imgMsg, 400)
|
|
8341
|
+
});
|
|
8342
|
+
return json({
|
|
8343
|
+
ok: true,
|
|
8344
|
+
linkedin: out2,
|
|
8345
|
+
usedImage: false,
|
|
8346
|
+
usedTextOnly: true,
|
|
8347
|
+
imagePathError: imgMsg,
|
|
8348
|
+
publishSummary: {
|
|
8349
|
+
mode: "text_only_after_image_failure",
|
|
8350
|
+
commentaryFrom,
|
|
8351
|
+
commentaryChars: commentary.length,
|
|
8352
|
+
linkedinPostId: out2.id ?? null,
|
|
8353
|
+
imageAttempt: {
|
|
8354
|
+
bytes: imagePayload.buffer.length,
|
|
8355
|
+
contentType: imagePayload.contentType,
|
|
8356
|
+
source: imagePayload.source
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
});
|
|
8360
|
+
}
|
|
8361
|
+
}
|
|
8362
|
+
logLinkedInPublish(publishLog, "no_image_bytes", {
|
|
8363
|
+
blogId,
|
|
8364
|
+
commentaryFrom,
|
|
8365
|
+
commentaryChars: commentary.length,
|
|
8366
|
+
note: "posting_text_only"
|
|
8367
|
+
});
|
|
8368
|
+
const out = await linkedInCreateTextShare({
|
|
8369
|
+
accessToken: token,
|
|
8370
|
+
personSub,
|
|
8371
|
+
commentary
|
|
8372
|
+
});
|
|
8373
|
+
logLinkedInPublish(publishLog, "success", {
|
|
8374
|
+
blogId,
|
|
8375
|
+
mode: "text_only",
|
|
8376
|
+
linkedinPostId: out.id ?? null,
|
|
8377
|
+
commentaryFrom,
|
|
8378
|
+
commentaryChars: commentary.length
|
|
8379
|
+
});
|
|
8380
|
+
return json({
|
|
8381
|
+
ok: true,
|
|
8382
|
+
linkedin: out,
|
|
8383
|
+
usedTextOnly: true,
|
|
8384
|
+
publishSummary: {
|
|
8385
|
+
mode: "text_only",
|
|
8386
|
+
commentaryFrom,
|
|
8387
|
+
commentaryChars: commentary.length,
|
|
8388
|
+
linkedinPostId: out.id ?? null
|
|
8389
|
+
},
|
|
8390
|
+
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."
|
|
8391
|
+
});
|
|
8392
|
+
} catch (e) {
|
|
8393
|
+
const msg = e instanceof Error ? e.message : "Publish failed";
|
|
8394
|
+
logLinkedInPublish(publishLog, "failed", { blogId, message: truncateForLog(msg, 600) });
|
|
8395
|
+
return json({ error: msg }, { status: 502 });
|
|
8396
|
+
}
|
|
8397
|
+
},
|
|
8398
|
+
async getFacebookStatus(req) {
|
|
8399
|
+
const a = await requireAuth(req);
|
|
8400
|
+
if (a) return a;
|
|
8401
|
+
const pe = await requireEntityPermission(req, "blogs", "read");
|
|
8402
|
+
if (pe) return pe;
|
|
8403
|
+
try {
|
|
8404
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8405
|
+
const enabled = map.enabled !== "false";
|
|
8406
|
+
const userToken = (map.meta_user_access_token ?? "").trim();
|
|
8407
|
+
const pageId = (map.meta_facebook_page_id ?? "").trim();
|
|
8408
|
+
let hasPageToken = false;
|
|
8409
|
+
if (enabled && userToken && pageId) {
|
|
8410
|
+
try {
|
|
8411
|
+
const accounts = await metaFetchUserManagedPages(userToken);
|
|
8412
|
+
hasPageToken = Boolean(metaResolvePageAccessToken(accounts, pageId));
|
|
8413
|
+
} catch {
|
|
8414
|
+
hasPageToken = false;
|
|
8415
|
+
}
|
|
8416
|
+
}
|
|
8417
|
+
const facebookReady = enabled && Boolean(userToken) && Boolean(pageId);
|
|
8418
|
+
return json({
|
|
8419
|
+
ok: true,
|
|
8420
|
+
facebookReady,
|
|
8421
|
+
enabled,
|
|
8422
|
+
hasUserToken: Boolean(userToken),
|
|
8423
|
+
hasPageId: Boolean(pageId),
|
|
8424
|
+
hasPageToken
|
|
8425
|
+
});
|
|
8426
|
+
} catch (e) {
|
|
8427
|
+
const msg = e instanceof Error ? e.message : "Failed to load status";
|
|
8428
|
+
return json({ error: msg }, { status: 500 });
|
|
8429
|
+
}
|
|
8430
|
+
},
|
|
8431
|
+
async publishBlogToFacebook(req, blogId) {
|
|
8432
|
+
const a = await requireAuth(req);
|
|
8433
|
+
if (a) return a;
|
|
8434
|
+
const pe = await requireEntityPermission(req, "blogs", "update");
|
|
8435
|
+
if (pe) return pe;
|
|
8436
|
+
if (!Number.isFinite(blogId) || blogId < 1) {
|
|
8437
|
+
return json({ error: "Invalid blog id" }, { status: 400 });
|
|
8438
|
+
}
|
|
8439
|
+
if (!entityMap.blogs) {
|
|
8440
|
+
return json({ error: "Blogs entity not configured" }, { status: 503 });
|
|
8441
|
+
}
|
|
8442
|
+
try {
|
|
8443
|
+
const map = await loadGroupMap(dataSource, entityMap, encryptionKey);
|
|
8444
|
+
if (map.enabled === "false") {
|
|
8445
|
+
return json({ error: "Social media plugin is disabled." }, { status: 400 });
|
|
8446
|
+
}
|
|
8447
|
+
const userToken = (map.meta_user_access_token ?? "").trim();
|
|
8448
|
+
const pageId = (map.meta_facebook_page_id ?? "").trim();
|
|
8449
|
+
if (!userToken || !pageId) {
|
|
8450
|
+
return json(
|
|
8451
|
+
{
|
|
8452
|
+
error: "Configure Meta user access token and Facebook Page ID in Plugins \u2192 Social media \u2192 Facebook, then save."
|
|
8453
|
+
},
|
|
8454
|
+
{ status: 400 }
|
|
8455
|
+
);
|
|
8456
|
+
}
|
|
8457
|
+
let accounts;
|
|
8458
|
+
try {
|
|
8459
|
+
accounts = await metaFetchUserManagedPages(userToken);
|
|
8460
|
+
} catch (e) {
|
|
8461
|
+
const msg = e instanceof Error ? e.message : "Failed to load Facebook pages";
|
|
8462
|
+
return json({ error: msg }, { status: 502 });
|
|
8463
|
+
}
|
|
8464
|
+
const pageAccessToken = metaResolvePageAccessToken(accounts, pageId);
|
|
8465
|
+
if (!pageAccessToken) {
|
|
8466
|
+
return json(
|
|
8467
|
+
{
|
|
8468
|
+
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."
|
|
8469
|
+
},
|
|
8470
|
+
{ status: 400 }
|
|
8471
|
+
);
|
|
8472
|
+
}
|
|
8473
|
+
const blogRepo = dataSource.getRepository(entityMap.blogs);
|
|
8474
|
+
const blog = await blogRepo.findOne({
|
|
8475
|
+
where: { id: blogId, deleted: false }
|
|
8476
|
+
});
|
|
8477
|
+
if (!blog) {
|
|
8478
|
+
return json({ error: "Blog not found" }, { status: 404 });
|
|
8479
|
+
}
|
|
8480
|
+
const b = blog;
|
|
8481
|
+
const title = String(b.title ?? "Blog").trim() || "Blog";
|
|
8482
|
+
const social = String(b.socialMediaContent ?? "").trim();
|
|
8483
|
+
const excerpt = stripHtml(String(b.content ?? ""), 2800);
|
|
8484
|
+
const caption = (social || `${title}
|
|
8485
|
+
|
|
8486
|
+
${excerpt}`).trim().slice(0, 2200);
|
|
8487
|
+
const imagePayload = await loadImageBytesForBlogShare(
|
|
8488
|
+
b.coverImage,
|
|
8489
|
+
String(b.content ?? ""),
|
|
8490
|
+
publicSiteUrl || inferRequestOrigin(req),
|
|
8491
|
+
req
|
|
8492
|
+
);
|
|
8493
|
+
if (imagePayload) {
|
|
8494
|
+
const out2 = await metaPostPagePhoto({
|
|
8495
|
+
pageId,
|
|
8496
|
+
pageAccessToken,
|
|
8497
|
+
imageBuffer: imagePayload.buffer,
|
|
8498
|
+
contentType: imagePayload.contentType,
|
|
8499
|
+
caption
|
|
8500
|
+
});
|
|
8501
|
+
return json({
|
|
8502
|
+
ok: true,
|
|
8503
|
+
facebook: out2,
|
|
8504
|
+
usedImage: true,
|
|
8505
|
+
publishSummary: {
|
|
8506
|
+
mode: "page_photo",
|
|
8507
|
+
captionChars: caption.length,
|
|
8508
|
+
imageSource: imagePayload.source,
|
|
8509
|
+
facebookPostId: out2.post_id ?? out2.id ?? null
|
|
8510
|
+
}
|
|
8511
|
+
});
|
|
8512
|
+
}
|
|
8513
|
+
const out = await metaPostPageFeed({
|
|
8514
|
+
pageId,
|
|
8515
|
+
pageAccessToken,
|
|
8516
|
+
message: caption
|
|
8517
|
+
});
|
|
8518
|
+
return json({
|
|
8519
|
+
ok: true,
|
|
8520
|
+
facebook: out,
|
|
8521
|
+
usedImage: false,
|
|
8522
|
+
usedFeed: true,
|
|
8523
|
+
publishSummary: {
|
|
8524
|
+
mode: "page_feed_text",
|
|
8525
|
+
messageChars: caption.length,
|
|
8526
|
+
facebookPostId: out.id ?? null
|
|
8527
|
+
},
|
|
8528
|
+
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."
|
|
8529
|
+
});
|
|
8530
|
+
} catch (e) {
|
|
8531
|
+
const msg = e instanceof Error ? e.message : "Facebook publish failed";
|
|
8532
|
+
return json({ error: msg }, { status: 502 });
|
|
8533
|
+
}
|
|
8534
|
+
},
|
|
8535
|
+
/**
|
|
8536
|
+
* POST JSON `{ appId, appSecret, userAccessToken }`. Calls Graph `GET /me/accounts` with the user token.
|
|
8537
|
+
* App ID and secret are validated but not used yet (reserved for future token flows).
|
|
8538
|
+
* Persist credentials via Plugins → Save: `meta_app_id` (public), `meta_app_secret` and `meta_user_access_token` (encrypted).
|
|
8539
|
+
*/
|
|
8540
|
+
async fetchFacebookManagedPages(req) {
|
|
8541
|
+
const a = await requireAuth(req);
|
|
8542
|
+
if (a) return a;
|
|
8543
|
+
const pe = await requireEntityPermission(req, "settings", "read");
|
|
8544
|
+
if (pe) return pe;
|
|
8545
|
+
let body;
|
|
8546
|
+
try {
|
|
8547
|
+
body = await req.json();
|
|
8548
|
+
} catch {
|
|
8549
|
+
return json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8550
|
+
}
|
|
8551
|
+
const appId = String(body?.appId ?? "").trim();
|
|
8552
|
+
const appSecret = String(body?.appSecret ?? "").trim();
|
|
8553
|
+
const userAccessToken = String(body?.userAccessToken ?? "").trim();
|
|
8554
|
+
if (!appId || !appSecret || !userAccessToken) {
|
|
8555
|
+
return json(
|
|
8556
|
+
{ error: "App ID, app secret, and user access token are required." },
|
|
8557
|
+
{ status: 400 }
|
|
8558
|
+
);
|
|
8559
|
+
}
|
|
8560
|
+
void appId;
|
|
8561
|
+
void appSecret;
|
|
8562
|
+
try {
|
|
8563
|
+
const graph = await metaFetchUserManagedPages(userAccessToken);
|
|
8564
|
+
return json({ ok: true, graph });
|
|
8565
|
+
} catch (e) {
|
|
8566
|
+
const msg = e instanceof Error ? e.message : "Facebook Graph request failed";
|
|
8567
|
+
return json({ error: msg }, { status: 502 });
|
|
8568
|
+
}
|
|
8569
|
+
}
|
|
8570
|
+
};
|
|
8571
|
+
}
|
|
8572
|
+
|
|
7228
8573
|
// src/api/cms-api-handler.ts
|
|
7229
8574
|
var KNOWLEDGE_SUFFIX = "knowledge";
|
|
7230
8575
|
var CMS_API_LOG = "[cms-api]";
|
|
@@ -7233,7 +8578,9 @@ function withLlmKnowledgeEntityFallbacks(base) {
|
|
|
7233
8578
|
"llm_agents",
|
|
7234
8579
|
"llm_agent_knowledge_documents",
|
|
7235
8580
|
"knowledge_base_documents",
|
|
7236
|
-
"knowledge_base_chunks"
|
|
8581
|
+
"knowledge_base_chunks",
|
|
8582
|
+
"rss_feeds",
|
|
8583
|
+
"rss_articles"
|
|
7237
8584
|
];
|
|
7238
8585
|
const patch = {};
|
|
7239
8586
|
for (const key of keys) {
|
|
@@ -7249,8 +8596,8 @@ function withLlmKnowledgeEntityFallbacks(base) {
|
|
|
7249
8596
|
}
|
|
7250
8597
|
return merged;
|
|
7251
8598
|
}
|
|
7252
|
-
function matchLlmAgentKnowledgeRoute(
|
|
7253
|
-
const p =
|
|
8599
|
+
function matchLlmAgentKnowledgeRoute(path2) {
|
|
8600
|
+
const p = path2[0] === "api" ? path2.slice(1) : path2;
|
|
7254
8601
|
if (p[0] !== "llm_agents" || p.length < 2) return null;
|
|
7255
8602
|
const seg1 = p[1];
|
|
7256
8603
|
if (!seg1) return null;
|
|
@@ -7281,7 +8628,9 @@ var DEFAULT_EXCLUDE = /* @__PURE__ */ new Set([
|
|
|
7281
8628
|
"wishlists",
|
|
7282
8629
|
"wishlist_items",
|
|
7283
8630
|
"message_templates",
|
|
7284
|
-
"llm_agent_knowledge_documents"
|
|
8631
|
+
"llm_agent_knowledge_documents",
|
|
8632
|
+
"rss_feeds",
|
|
8633
|
+
"rss_articles"
|
|
7285
8634
|
]);
|
|
7286
8635
|
function createCmsApiHandler(config) {
|
|
7287
8636
|
const {
|
|
@@ -7408,6 +8757,23 @@ function createCmsApiHandler(config) {
|
|
|
7408
8757
|
const avatarPost = userAvatar ? createUserAvatarHandler(userAvatar) : null;
|
|
7409
8758
|
const profileHandlers = userProfile ? createUserProfileHandler(userProfile) : null;
|
|
7410
8759
|
const settingsHandlers = settingsConfig ? createSettingsApiHandlers(settingsConfig) : null;
|
|
8760
|
+
function resolvePublicSiteUrlForSocial() {
|
|
8761
|
+
if (typeof process === "undefined") return void 0;
|
|
8762
|
+
const u = (process.env.CMS_PUBLIC_URL || process.env.NEXT_PUBLIC_SITE_URL || "").trim();
|
|
8763
|
+
if (u) return u.replace(/\/+$/, "");
|
|
8764
|
+
const v = (process.env.VERCEL_URL || "").trim();
|
|
8765
|
+
if (!v) return void 0;
|
|
8766
|
+
return v.startsWith("http") ? v.replace(/\/+$/, "") : `https://${v.replace(/\/+$/, "")}`;
|
|
8767
|
+
}
|
|
8768
|
+
const socialMediaHandlers = settingsConfig?.dataSource && entityMap.configs ? createSocialMediaHandlers({
|
|
8769
|
+
dataSource: settingsConfig.dataSource,
|
|
8770
|
+
entityMap,
|
|
8771
|
+
json: config.json,
|
|
8772
|
+
requireAuth: config.requireAuth,
|
|
8773
|
+
requireEntityPermission: requireEntityPermissionEffective,
|
|
8774
|
+
encryptionKey: settingsConfig.encryptionKey,
|
|
8775
|
+
publicSiteUrl: resolvePublicSiteUrlForSocial()
|
|
8776
|
+
}) : null;
|
|
7411
8777
|
const smsMessageTemplateHandlers = createSmsMessageTemplateHandlers({
|
|
7412
8778
|
dataSource,
|
|
7413
8779
|
entityMap,
|
|
@@ -7434,95 +8800,526 @@ function createCmsApiHandler(config) {
|
|
|
7434
8800
|
return {
|
|
7435
8801
|
async handle(method, pathInput, req) {
|
|
7436
8802
|
const m = typeof method === "string" ? method.toUpperCase() : "GET";
|
|
7437
|
-
const
|
|
8803
|
+
const path2 = pathInput.length > 0 && pathInput[0] === "api" ? pathInput.slice(1) : pathInput;
|
|
7438
8804
|
async function analyticsGate() {
|
|
7439
8805
|
const a = await config.requireAuth(req);
|
|
7440
8806
|
if (a) return a;
|
|
7441
8807
|
return requireEntityPermissionEffective(req, "analytics", "read");
|
|
7442
8808
|
}
|
|
7443
|
-
if (
|
|
8809
|
+
if (path2[0] === "admin" && path2[1] === "roles") {
|
|
7444
8810
|
if (!adminRoles) return config.json({ error: "Not found" }, { status: 404 });
|
|
7445
|
-
if (
|
|
7446
|
-
if (
|
|
7447
|
-
if (
|
|
7448
|
-
if (
|
|
7449
|
-
if (
|
|
8811
|
+
if (path2.length === 2 && m === "GET") return adminRoles.list();
|
|
8812
|
+
if (path2.length === 2 && m === "POST") return adminRoles.createGroup(req);
|
|
8813
|
+
if (path2.length === 3 && m === "PATCH") return adminRoles.patchGroup(req, path2[2]);
|
|
8814
|
+
if (path2.length === 3 && m === "DELETE") return adminRoles.deleteGroup(path2[2]);
|
|
8815
|
+
if (path2.length === 4 && path2[3] === "permissions" && m === "PUT") return adminRoles.putPermissions(req, path2[2]);
|
|
7450
8816
|
return config.json({ error: "Not found" }, { status: 404 });
|
|
7451
8817
|
}
|
|
7452
|
-
if (
|
|
8818
|
+
if (path2[0] === "dashboard" && path2[1] === "stats" && path2.length === 2 && m === "GET" && dashboardGet) {
|
|
7453
8819
|
return dashboardGet(req);
|
|
7454
8820
|
}
|
|
7455
|
-
if (
|
|
8821
|
+
if (path2[0] === "dashboard" && path2[1] === "ecommerce" && path2.length === 2 && m === "GET" && ecommerceAnalyticsGet) {
|
|
7456
8822
|
const g = await analyticsGate();
|
|
7457
8823
|
if (g) return g;
|
|
7458
8824
|
return ecommerceAnalyticsGet(req);
|
|
7459
8825
|
}
|
|
7460
|
-
if (
|
|
7461
|
-
|
|
8826
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 3 && m === "DELETE" && getCms) {
|
|
8827
|
+
const id = path2[2];
|
|
8828
|
+
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);
|
|
8829
|
+
const a = await config.requireAuth(req);
|
|
8830
|
+
if (a) return a;
|
|
8831
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8832
|
+
if (pe) return pe;
|
|
8833
|
+
if (!entityMap.rss_feeds) {
|
|
8834
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8835
|
+
}
|
|
8836
|
+
if (!uuidLooksValid) {
|
|
8837
|
+
return config.json({ error: "Invalid feed id" }, { status: 400 });
|
|
8838
|
+
}
|
|
8839
|
+
try {
|
|
8840
|
+
await dataSource.getRepository(entityMap.rss_feeds).delete({ id });
|
|
8841
|
+
return config.json({ ok: true });
|
|
8842
|
+
} catch (e) {
|
|
8843
|
+
const message = e instanceof Error ? e.message : "Delete failed";
|
|
8844
|
+
return config.json({ error: message }, { status: 500 });
|
|
8845
|
+
}
|
|
8846
|
+
}
|
|
8847
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 2 && m === "GET" && getCms) {
|
|
8848
|
+
const a = await config.requireAuth(req);
|
|
8849
|
+
if (a) return a;
|
|
8850
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8851
|
+
if (pe) return pe;
|
|
8852
|
+
if (!entityMap.rss_feeds) {
|
|
8853
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8854
|
+
}
|
|
8855
|
+
try {
|
|
8856
|
+
const list = await dataSource.getRepository(entityMap.rss_feeds).find({
|
|
8857
|
+
where: { isActive: true },
|
|
8858
|
+
order: { updatedAt: "DESC" }
|
|
8859
|
+
});
|
|
8860
|
+
return config.json({ feeds: list.map(serializeRssFeed) });
|
|
8861
|
+
} catch (e) {
|
|
8862
|
+
const message = e instanceof Error ? e.message : "Failed to list feeds";
|
|
8863
|
+
return config.json({ error: message }, { status: 500 });
|
|
8864
|
+
}
|
|
8865
|
+
}
|
|
8866
|
+
if (path2[0] === "blog-generator" && path2[1] === "feeds" && path2.length === 2 && m === "POST" && getCms) {
|
|
8867
|
+
const a = await config.requireAuth(req);
|
|
8868
|
+
if (a) return a;
|
|
8869
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8870
|
+
if (pe) return pe;
|
|
8871
|
+
if (!entityMap.rss_feeds) {
|
|
8872
|
+
return config.json({ error: "RSS feeds are not configured (missing entity map)." }, { status: 503 });
|
|
8873
|
+
}
|
|
8874
|
+
let body;
|
|
8875
|
+
try {
|
|
8876
|
+
body = await req.json();
|
|
8877
|
+
} catch {
|
|
8878
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8879
|
+
}
|
|
8880
|
+
const rowsIn = Array.isArray(body.rows) ? body.rows : [];
|
|
8881
|
+
const rows = rowsIn.map((r) => r).map((r) => {
|
|
8882
|
+
let websiteUrl;
|
|
8883
|
+
if ("websiteUrl" in r) {
|
|
8884
|
+
const w = r.websiteUrl;
|
|
8885
|
+
if (w === null || w === "") websiteUrl = null;
|
|
8886
|
+
else if (typeof w === "string") websiteUrl = w;
|
|
8887
|
+
}
|
|
8888
|
+
return {
|
|
8889
|
+
rssUrl: typeof r.rssUrl === "string" ? r.rssUrl.trim() : "",
|
|
8890
|
+
name: typeof r.name === "string" ? r.name : void 0,
|
|
8891
|
+
websiteUrl
|
|
8892
|
+
};
|
|
8893
|
+
}).filter((r) => r.rssUrl !== "");
|
|
8894
|
+
try {
|
|
8895
|
+
const Feed = entityMap.rss_feeds;
|
|
8896
|
+
for (const r of rows) {
|
|
8897
|
+
await upsertRssFeedRow(dataSource, Feed, r.rssUrl, {
|
|
8898
|
+
name: r.name,
|
|
8899
|
+
websiteUrl: r.websiteUrl
|
|
8900
|
+
});
|
|
8901
|
+
}
|
|
8902
|
+
const list = await dataSource.getRepository(Feed).find({
|
|
8903
|
+
where: { isActive: true },
|
|
8904
|
+
order: { updatedAt: "DESC" }
|
|
8905
|
+
});
|
|
8906
|
+
return config.json({ feeds: list.map(serializeRssFeed) });
|
|
8907
|
+
} catch (e) {
|
|
8908
|
+
const message = e instanceof Error ? e.message : "Failed to save feeds";
|
|
8909
|
+
return config.json({ error: message }, { status: 500 });
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
if (path2[0] === "blog-generator" && path2[1] === "latest" && path2.length === 2 && m === "POST" && getCms) {
|
|
8913
|
+
const a = await config.requireAuth(req);
|
|
8914
|
+
if (a) return a;
|
|
8915
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8916
|
+
if (pe) return pe;
|
|
8917
|
+
const cms = await getCms();
|
|
8918
|
+
const svc = cms.getPlugin("blog_generator");
|
|
8919
|
+
if (!svc) {
|
|
8920
|
+
return config.json({ error: "Blog Generator plugin is not enabled" }, { status: 503 });
|
|
8921
|
+
}
|
|
8922
|
+
let body;
|
|
8923
|
+
try {
|
|
8924
|
+
body = await req.json();
|
|
8925
|
+
} catch {
|
|
8926
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
8927
|
+
}
|
|
8928
|
+
const rawUrls = [];
|
|
8929
|
+
if (Array.isArray(body.rssUrls)) {
|
|
8930
|
+
for (const u of body.rssUrls) {
|
|
8931
|
+
if (typeof u === "string") {
|
|
8932
|
+
const t = u.trim();
|
|
8933
|
+
if (t) rawUrls.push(t);
|
|
8934
|
+
}
|
|
8935
|
+
}
|
|
8936
|
+
}
|
|
8937
|
+
if (rawUrls.length === 0 && typeof body.rssUrl === "string" && body.rssUrl.trim()) {
|
|
8938
|
+
rawUrls.push(body.rssUrl.trim());
|
|
8939
|
+
}
|
|
8940
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8941
|
+
const urls = rawUrls.filter((u) => {
|
|
8942
|
+
if (seen.has(u)) return false;
|
|
8943
|
+
seen.add(u);
|
|
8944
|
+
return true;
|
|
8945
|
+
});
|
|
8946
|
+
if (urls.length === 0) {
|
|
8947
|
+
return config.json({ error: "Provide rssUrls (array) or rssUrl (string)" }, { status: 400 });
|
|
8948
|
+
}
|
|
8949
|
+
const results = [];
|
|
8950
|
+
for (const rssUrl of urls) {
|
|
8951
|
+
try {
|
|
8952
|
+
if (entityMap.rss_feeds) {
|
|
8953
|
+
await upsertRssFeedRow(dataSource, entityMap.rss_feeds, rssUrl);
|
|
8954
|
+
}
|
|
8955
|
+
const latest = await svc.getLatestArticleFromFeed(rssUrl);
|
|
8956
|
+
if (entityMap.rss_feeds) {
|
|
8957
|
+
await recordRssFetchSuccess(
|
|
8958
|
+
dataSource,
|
|
8959
|
+
entityMap.rss_feeds,
|
|
8960
|
+
entityMap.rss_articles,
|
|
8961
|
+
rssUrl,
|
|
8962
|
+
latest == null ? null : {
|
|
8963
|
+
title: latest.title,
|
|
8964
|
+
link: latest.link,
|
|
8965
|
+
summary: latest.summary,
|
|
8966
|
+
content: latest.content,
|
|
8967
|
+
creator: latest.creator,
|
|
8968
|
+
date: latest.date
|
|
8969
|
+
}
|
|
8970
|
+
);
|
|
8971
|
+
}
|
|
8972
|
+
const article = latest == null ? null : {
|
|
8973
|
+
title: latest.title,
|
|
8974
|
+
link: latest.link,
|
|
8975
|
+
summary: latest.summary,
|
|
8976
|
+
content: latest.content,
|
|
8977
|
+
date: latest.date.toISOString()
|
|
8978
|
+
};
|
|
8979
|
+
results.push({ rssUrl, article });
|
|
8980
|
+
} catch (e) {
|
|
8981
|
+
const message = e instanceof Error ? e.message : "Failed to parse feed";
|
|
8982
|
+
results.push({ rssUrl, article: null, error: message });
|
|
8983
|
+
}
|
|
8984
|
+
}
|
|
8985
|
+
const singleArticle = results.length === 1 ? results[0].article : void 0;
|
|
8986
|
+
return config.json({ results, article: singleArticle });
|
|
8987
|
+
}
|
|
8988
|
+
if (path2[0] === "blog-generator" && path2[1] === "generate" && path2.length === 2 && m === "POST" && getCms) {
|
|
8989
|
+
const a = await config.requireAuth(req);
|
|
8990
|
+
if (a) return a;
|
|
8991
|
+
const pe = await requireEntityPermissionEffective(req, "blogs", "read");
|
|
8992
|
+
if (pe) return pe;
|
|
8993
|
+
const cms = await getCms();
|
|
8994
|
+
const svc = cms.getPlugin("blog_generator");
|
|
8995
|
+
if (!svc) {
|
|
8996
|
+
return config.json({ error: "Blog Generator plugin is not enabled" }, { status: 503 });
|
|
8997
|
+
}
|
|
8998
|
+
const llm = cms.getPlugin("llm");
|
|
8999
|
+
if (!llm) {
|
|
9000
|
+
return config.json({ error: "LLM plugin is not enabled. Configure the LLM gateway to generate articles." }, { status: 503 });
|
|
9001
|
+
}
|
|
9002
|
+
let body;
|
|
9003
|
+
try {
|
|
9004
|
+
body = await req.json();
|
|
9005
|
+
} catch {
|
|
9006
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9007
|
+
}
|
|
9008
|
+
const rawUrls = [];
|
|
9009
|
+
if (Array.isArray(body.rssUrls)) {
|
|
9010
|
+
for (const u of body.rssUrls) {
|
|
9011
|
+
if (typeof u === "string") {
|
|
9012
|
+
const t = u.trim();
|
|
9013
|
+
if (t) rawUrls.push(t);
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9017
|
+
if (rawUrls.length === 0 && typeof body.rssUrl === "string" && body.rssUrl.trim()) {
|
|
9018
|
+
rawUrls.push(body.rssUrl.trim());
|
|
9019
|
+
}
|
|
9020
|
+
const seen = /* @__PURE__ */ new Set();
|
|
9021
|
+
const rssUrls = rawUrls.filter((u) => {
|
|
9022
|
+
if (seen.has(u)) return false;
|
|
9023
|
+
seen.add(u);
|
|
9024
|
+
return true;
|
|
9025
|
+
});
|
|
9026
|
+
if (rssUrls.length === 0) {
|
|
9027
|
+
return config.json({ error: "Provide rssUrls (non-empty array) or rssUrl (string)" }, { status: 400 });
|
|
9028
|
+
}
|
|
9029
|
+
const systemInstruction = typeof body.systemInstruction === "string" ? body.systemInstruction : void 0;
|
|
9030
|
+
const validationRules = typeof body.validationRules === "string" ? body.validationRules : void 0;
|
|
9031
|
+
const persistToCms = body.persistToCms === true;
|
|
9032
|
+
let llmAgentChatOptions;
|
|
9033
|
+
let llmAgentResolution = null;
|
|
9034
|
+
let metadataLlmAgentChatOptions;
|
|
9035
|
+
let metadataLlmAgentResolution = null;
|
|
9036
|
+
let socialLlmAgentChatOptions;
|
|
9037
|
+
let socialLlmAgentResolution = null;
|
|
9038
|
+
let metadataRow = null;
|
|
9039
|
+
let socialRow = null;
|
|
9040
|
+
if (entityMap.llm_agents) {
|
|
9041
|
+
const agentRepo = dataSource.getRepository(entityMap.llm_agents);
|
|
9042
|
+
const agentRow = await agentRepo.findOne({
|
|
9043
|
+
where: { slug: BLOG_GENERATOR_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9044
|
+
});
|
|
9045
|
+
if (agentRow) {
|
|
9046
|
+
const o = llmAgentToChatAgentOptions(agentRow);
|
|
9047
|
+
llmAgentChatOptions = {
|
|
9048
|
+
model: o.model,
|
|
9049
|
+
temperature: o.temperature,
|
|
9050
|
+
max_tokens: o.max_tokens
|
|
9051
|
+
};
|
|
9052
|
+
llmAgentResolution = {
|
|
9053
|
+
slug: BLOG_GENERATOR_LLM_AGENT_SLUG,
|
|
9054
|
+
model: agentRow.model?.trim() || null,
|
|
9055
|
+
temperature: agentRow.temperature ?? null,
|
|
9056
|
+
maxTokens: agentRow.maxTokens ?? null
|
|
9057
|
+
};
|
|
9058
|
+
}
|
|
9059
|
+
metadataRow = await agentRepo.findOne({
|
|
9060
|
+
where: { slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9061
|
+
});
|
|
9062
|
+
if (metadataRow) {
|
|
9063
|
+
const mo = llmAgentToChatAgentOptions(metadataRow);
|
|
9064
|
+
metadataLlmAgentChatOptions = {
|
|
9065
|
+
model: mo.model,
|
|
9066
|
+
temperature: mo.temperature,
|
|
9067
|
+
max_tokens: mo.max_tokens
|
|
9068
|
+
};
|
|
9069
|
+
metadataLlmAgentResolution = {
|
|
9070
|
+
slug: BLOG_METADATA_ENRICHER_LLM_AGENT_SLUG,
|
|
9071
|
+
model: metadataRow.model?.trim() || null,
|
|
9072
|
+
temperature: metadataRow.temperature ?? null,
|
|
9073
|
+
maxTokens: metadataRow.maxTokens ?? null
|
|
9074
|
+
};
|
|
9075
|
+
}
|
|
9076
|
+
socialRow = await agentRepo.findOne({
|
|
9077
|
+
where: { slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG, deleted: false, enabled: true }
|
|
9078
|
+
});
|
|
9079
|
+
if (socialRow) {
|
|
9080
|
+
const so = llmAgentToChatAgentOptions(socialRow);
|
|
9081
|
+
socialLlmAgentChatOptions = {
|
|
9082
|
+
model: so.model,
|
|
9083
|
+
temperature: so.temperature,
|
|
9084
|
+
max_tokens: so.max_tokens
|
|
9085
|
+
};
|
|
9086
|
+
socialLlmAgentResolution = {
|
|
9087
|
+
slug: BLOG_SOCIAL_ENRICHER_LLM_AGENT_SLUG,
|
|
9088
|
+
model: socialRow.model?.trim() || null,
|
|
9089
|
+
temperature: socialRow.temperature ?? null,
|
|
9090
|
+
maxTokens: socialRow.maxTokens ?? null
|
|
9091
|
+
};
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
try {
|
|
9095
|
+
const categoryEntities = entityMap.categories ? await dataSource.getRepository(entityMap.categories).find({
|
|
9096
|
+
where: { deleted: false },
|
|
9097
|
+
select: ["id", "name"]
|
|
9098
|
+
}) : [];
|
|
9099
|
+
const categoryRows = categoryEntities.map((r) => ({
|
|
9100
|
+
id: Number(r.id),
|
|
9101
|
+
name: String(r.name ?? "").trim()
|
|
9102
|
+
})).filter((r) => Number.isFinite(r.id) && r.name !== "");
|
|
9103
|
+
const tagEntities = entityMap.tags ? await dataSource.getRepository(entityMap.tags).find({
|
|
9104
|
+
where: { deleted: false },
|
|
9105
|
+
select: ["name"]
|
|
9106
|
+
}) : [];
|
|
9107
|
+
const tagNames = tagEntities.map((r) => String(r.name ?? "").trim()).filter((n) => n !== "");
|
|
9108
|
+
const out = await svc.generateBlogMarkdownFromRss({
|
|
9109
|
+
llm,
|
|
9110
|
+
rssUrls,
|
|
9111
|
+
systemInstruction,
|
|
9112
|
+
validationRules,
|
|
9113
|
+
categoryNamesHint: categoryRows.map((c) => c.name),
|
|
9114
|
+
tagNamesHint: tagNames,
|
|
9115
|
+
llmAgentChatOptions,
|
|
9116
|
+
metadataSystemInstruction: metadataRow?.systemInstruction,
|
|
9117
|
+
metadataValidationRules: metadataRow?.validationRules ?? void 0,
|
|
9118
|
+
metadataLlmAgentChatOptions,
|
|
9119
|
+
socialSystemInstruction: socialRow?.systemInstruction,
|
|
9120
|
+
socialValidationRules: socialRow?.validationRules ?? void 0,
|
|
9121
|
+
socialLlmAgentChatOptions
|
|
9122
|
+
});
|
|
9123
|
+
let savedBlogs;
|
|
9124
|
+
if (persistToCms) {
|
|
9125
|
+
const pCreate = await requireEntityPermissionEffective(req, "blogs", "create");
|
|
9126
|
+
if (pCreate) return pCreate;
|
|
9127
|
+
if (!entityMap.blogs || !entityMap.tags || !entityMap.categories || !entityMap.seos) {
|
|
9128
|
+
return config.json(
|
|
9129
|
+
{ error: "persistToCms requires blogs, tags, categories, and seos in the entity map." },
|
|
9130
|
+
{ status: 503 }
|
|
9131
|
+
);
|
|
9132
|
+
}
|
|
9133
|
+
let authorId = null;
|
|
9134
|
+
if (getSessionUser) {
|
|
9135
|
+
const su = await getSessionUser();
|
|
9136
|
+
if (su?.id) {
|
|
9137
|
+
const n = Number(su.id);
|
|
9138
|
+
if (Number.isFinite(n)) authorId = n;
|
|
9139
|
+
}
|
|
9140
|
+
if (authorId == null && su?.email?.trim() && entityMap.users) {
|
|
9141
|
+
const ur = await dataSource.getRepository(entityMap.users).findOne({
|
|
9142
|
+
where: { email: su.email.trim(), deleted: false },
|
|
9143
|
+
select: ["id"]
|
|
9144
|
+
});
|
|
9145
|
+
if (ur) authorId = Number(ur.id);
|
|
9146
|
+
}
|
|
9147
|
+
}
|
|
9148
|
+
if (authorId == null || !Number.isFinite(authorId)) {
|
|
9149
|
+
return config.json(
|
|
9150
|
+
{
|
|
9151
|
+
error: "persistToCms requires a logged-in user with id in session or a users row matching session email."
|
|
9152
|
+
},
|
|
9153
|
+
{ status: 400 }
|
|
9154
|
+
);
|
|
9155
|
+
}
|
|
9156
|
+
savedBlogs = await persistAllGeneratedBlogDrafts(
|
|
9157
|
+
dataSource,
|
|
9158
|
+
{
|
|
9159
|
+
blogs: entityMap.blogs,
|
|
9160
|
+
tags: entityMap.tags,
|
|
9161
|
+
categories: entityMap.categories,
|
|
9162
|
+
seos: entityMap.seos
|
|
9163
|
+
},
|
|
9164
|
+
{ drafts: out.blogDrafts, authorId }
|
|
9165
|
+
);
|
|
9166
|
+
}
|
|
9167
|
+
const blogs = out.blogDrafts.map((draft) => {
|
|
9168
|
+
const { categoryId, matched: categoryMatched } = resolveBlogCategoryIdByName(
|
|
9169
|
+
draft.categoryName,
|
|
9170
|
+
categoryRows
|
|
9171
|
+
);
|
|
9172
|
+
return {
|
|
9173
|
+
title: draft.title,
|
|
9174
|
+
slug: draft.slug ?? null,
|
|
9175
|
+
markdown: draft.markdown,
|
|
9176
|
+
socialMediaContent: draft.socialMediaContent ?? null,
|
|
9177
|
+
categoryName: draft.categoryName,
|
|
9178
|
+
categoryId,
|
|
9179
|
+
categoryMatched,
|
|
9180
|
+
seo: draft.seo,
|
|
9181
|
+
tags: draft.tags,
|
|
9182
|
+
parseMode: draft.parseMode
|
|
9183
|
+
};
|
|
9184
|
+
});
|
|
9185
|
+
const firstArticle = out.article;
|
|
9186
|
+
return config.json({
|
|
9187
|
+
agentName: out.agentName,
|
|
9188
|
+
blogMarkdown: out.blogMarkdown,
|
|
9189
|
+
blogs,
|
|
9190
|
+
blog: blogs[0] ?? null,
|
|
9191
|
+
feedArticles: out.feedArticles.map((f) => ({
|
|
9192
|
+
rssUrl: f.rssUrl,
|
|
9193
|
+
article: {
|
|
9194
|
+
title: f.article.title,
|
|
9195
|
+
link: f.article.link,
|
|
9196
|
+
summary: f.article.summary,
|
|
9197
|
+
content: f.article.content,
|
|
9198
|
+
date: f.article.date.toISOString()
|
|
9199
|
+
}
|
|
9200
|
+
})),
|
|
9201
|
+
llmAgent: llmAgentResolution,
|
|
9202
|
+
metadataLlmAgent: metadataLlmAgentResolution,
|
|
9203
|
+
socialLlmAgent: socialLlmAgentResolution,
|
|
9204
|
+
article: {
|
|
9205
|
+
title: firstArticle.title,
|
|
9206
|
+
link: firstArticle.link,
|
|
9207
|
+
summary: firstArticle.summary,
|
|
9208
|
+
content: firstArticle.content,
|
|
9209
|
+
date: firstArticle.date.toISOString()
|
|
9210
|
+
},
|
|
9211
|
+
...savedBlogs != null ? { savedBlogs } : {}
|
|
9212
|
+
});
|
|
9213
|
+
} catch (e) {
|
|
9214
|
+
const message = e instanceof Error ? e.message : "Failed to generate blog";
|
|
9215
|
+
const status = /No items found|LLM returned empty/i.test(message) ? 400 : 502;
|
|
9216
|
+
return config.json({ error: message }, { status });
|
|
9217
|
+
}
|
|
9218
|
+
}
|
|
9219
|
+
if (path2[0] === "analytics" && analyticsHandlers) {
|
|
9220
|
+
if (path2.length === 1 && m === "GET") {
|
|
7462
9221
|
const g = await analyticsGate();
|
|
7463
9222
|
if (g) return g;
|
|
7464
9223
|
return analyticsHandlers.GET(req);
|
|
7465
9224
|
}
|
|
7466
|
-
if (
|
|
9225
|
+
if (path2.length === 2 && path2[1] === "property-id" && m === "GET") {
|
|
7467
9226
|
const g = await analyticsGate();
|
|
7468
9227
|
if (g) return g;
|
|
7469
9228
|
return analyticsHandlers.propertyId();
|
|
7470
9229
|
}
|
|
7471
|
-
if (
|
|
9230
|
+
if (path2.length === 2 && path2[1] === "permissions" && m === "GET") {
|
|
7472
9231
|
const g = await analyticsGate();
|
|
7473
9232
|
if (g) return g;
|
|
7474
9233
|
return analyticsHandlers.permissions();
|
|
7475
9234
|
}
|
|
7476
9235
|
}
|
|
7477
|
-
if (
|
|
7478
|
-
if (
|
|
7479
|
-
return zipExtractPost(req,
|
|
9236
|
+
if (path2[0] === "upload" && path2.length === 1 && m === "POST" && uploadPost) return uploadPost(req);
|
|
9237
|
+
if (path2[0] === "media" && path2[1] === "extract" && path2.length === 3 && m === "POST" && zipExtractPost) {
|
|
9238
|
+
return zipExtractPost(req, path2[2]);
|
|
7480
9239
|
}
|
|
7481
|
-
if (
|
|
7482
|
-
return blogBySlugGet(req,
|
|
9240
|
+
if (path2[0] === "blogs" && path2[1] === "slug" && path2.length === 3 && m === "GET" && blogBySlugGet) {
|
|
9241
|
+
return blogBySlugGet(req, path2[2]);
|
|
7483
9242
|
}
|
|
7484
|
-
if (
|
|
7485
|
-
return formBySlugGet(req,
|
|
9243
|
+
if (path2[0] === "forms" && path2[1] === "slug" && path2.length === 3 && m === "GET" && formBySlugGet) {
|
|
9244
|
+
return formBySlugGet(req, path2[2]);
|
|
7486
9245
|
}
|
|
7487
|
-
if (
|
|
7488
|
-
if (
|
|
9246
|
+
if (path2[0] === "form-submissions") {
|
|
9247
|
+
if (path2.length === 1) {
|
|
7489
9248
|
if (m === "GET" && formSubmissionList) return formSubmissionList(req);
|
|
7490
9249
|
if (m === "POST" && formSubmissionPost) return formSubmissionPost(req);
|
|
7491
9250
|
}
|
|
7492
|
-
if (
|
|
9251
|
+
if (path2.length === 2 && m === "GET" && formSubmissionGetById) return formSubmissionGetById(req, path2[1]);
|
|
7493
9252
|
}
|
|
7494
|
-
if (
|
|
7495
|
-
if (
|
|
7496
|
-
if (
|
|
7497
|
-
if (m === "GET") return formSaveHandlers.GET(req,
|
|
7498
|
-
if (m === "PUT" || m === "PATCH") return formSaveHandlers.PUT(req,
|
|
9253
|
+
if (path2[0] === "forms" && formSaveHandlers) {
|
|
9254
|
+
if (path2.length === 1 && m === "POST") return formSaveHandlers.POST(req);
|
|
9255
|
+
if (path2.length === 2) {
|
|
9256
|
+
if (m === "GET") return formSaveHandlers.GET(req, path2[1]);
|
|
9257
|
+
if (m === "PUT" || m === "PATCH") return formSaveHandlers.PUT(req, path2[1]);
|
|
7499
9258
|
}
|
|
7500
9259
|
}
|
|
7501
|
-
if (
|
|
7502
|
-
if (
|
|
9260
|
+
if (path2[0] === "users" && usersHandlers) {
|
|
9261
|
+
if (path2.length === 1) {
|
|
7503
9262
|
if (m === "GET") return usersHandlers.list(req);
|
|
7504
9263
|
if (m === "POST") return usersHandlers.create(req);
|
|
7505
9264
|
}
|
|
7506
|
-
if (
|
|
7507
|
-
if (
|
|
7508
|
-
if (
|
|
9265
|
+
if (path2.length === 2) {
|
|
9266
|
+
if (path2[1] === "avatar" && m === "POST" && avatarPost) return avatarPost(req);
|
|
9267
|
+
if (path2[1] === "profile" && profileHandlers) {
|
|
7509
9268
|
if (m === "GET") return profileHandlers.GET(req);
|
|
7510
9269
|
if (m === "PUT") return profileHandlers.PUT(req);
|
|
7511
9270
|
}
|
|
7512
|
-
const id =
|
|
9271
|
+
const id = path2[1];
|
|
7513
9272
|
if (m === "GET") return usersHandlers.getById(req, id);
|
|
7514
9273
|
if (m === "PUT" || m === "PATCH") return usersHandlers.update(req, id);
|
|
7515
9274
|
if (m === "DELETE") return usersHandlers.delete(req, id);
|
|
7516
9275
|
}
|
|
7517
|
-
if (
|
|
7518
|
-
return usersHandlers.regenerateInvite(req,
|
|
9276
|
+
if (path2.length === 3 && path2[2] === "regenerate-invite" && m === "POST") {
|
|
9277
|
+
return usersHandlers.regenerateInvite(req, path2[1]);
|
|
7519
9278
|
}
|
|
7520
9279
|
}
|
|
7521
|
-
if (
|
|
7522
|
-
return userAuthRouter.POST(req,
|
|
9280
|
+
if (path2[0] === "users" && path2.length === 2 && userAuthRouter && m === "POST") {
|
|
9281
|
+
return userAuthRouter.POST(req, path2[1]);
|
|
9282
|
+
}
|
|
9283
|
+
if (path2[0] === "social-media" && path2[1] === "linkedin" && socialMediaHandlers && path2.length === 3) {
|
|
9284
|
+
const tail = path2[2];
|
|
9285
|
+
if (tail === "status" && m === "GET") {
|
|
9286
|
+
return socialMediaHandlers.getLinkedInStatus(req);
|
|
9287
|
+
}
|
|
9288
|
+
if (tail === "sync-profile" && m === "POST") {
|
|
9289
|
+
return socialMediaHandlers.syncLinkedInProfile(req);
|
|
9290
|
+
}
|
|
9291
|
+
if (tail === "publish-blog" && m === "POST") {
|
|
9292
|
+
let body;
|
|
9293
|
+
try {
|
|
9294
|
+
body = await req.json();
|
|
9295
|
+
} catch {
|
|
9296
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9297
|
+
}
|
|
9298
|
+
const id = Number(body?.blogId);
|
|
9299
|
+
return socialMediaHandlers.publishBlogToLinkedIn(req, id);
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
if (path2[0] === "social-media" && path2[1] === "facebook" && socialMediaHandlers && path2.length === 3) {
|
|
9303
|
+
const tail = path2[2];
|
|
9304
|
+
if (tail === "fetch-pages" && m === "POST") {
|
|
9305
|
+
return socialMediaHandlers.fetchFacebookManagedPages(req);
|
|
9306
|
+
}
|
|
9307
|
+
if (tail === "status" && m === "GET") {
|
|
9308
|
+
return socialMediaHandlers.getFacebookStatus(req);
|
|
9309
|
+
}
|
|
9310
|
+
if (tail === "publish-blog" && m === "POST") {
|
|
9311
|
+
let body;
|
|
9312
|
+
try {
|
|
9313
|
+
body = await req.json();
|
|
9314
|
+
} catch {
|
|
9315
|
+
return config.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
9316
|
+
}
|
|
9317
|
+
const id = Number(body?.blogId);
|
|
9318
|
+
return socialMediaHandlers.publishBlogToFacebook(req, id);
|
|
9319
|
+
}
|
|
7523
9320
|
}
|
|
7524
|
-
if (
|
|
7525
|
-
const group =
|
|
9321
|
+
if (path2[0] === "settings" && path2.length === 2 && settingsHandlers) {
|
|
9322
|
+
const group = path2[1];
|
|
7526
9323
|
const isPublic = settingsConfig?.publicGetGroups?.includes(group);
|
|
7527
9324
|
if (m === "GET") {
|
|
7528
9325
|
if (!isPublic) {
|
|
@@ -7539,15 +9336,15 @@ function createCmsApiHandler(config) {
|
|
|
7539
9336
|
return settingsHandlers.PUT(req, group);
|
|
7540
9337
|
}
|
|
7541
9338
|
}
|
|
7542
|
-
if (
|
|
9339
|
+
if (path2[0] === "message-templates" && path2[1] === "sms" && path2.length === 2) {
|
|
7543
9340
|
if (m === "GET") return smsMessageTemplateHandlers.GET(req);
|
|
7544
9341
|
if (m === "PUT") return smsMessageTemplateHandlers.PUT(req);
|
|
7545
9342
|
}
|
|
7546
|
-
if (
|
|
9343
|
+
if (path2[0] === "llm_agents") {
|
|
7547
9344
|
if (!entityMap.llm_agents) {
|
|
7548
9345
|
console.error(CMS_API_LOG, "llm_agents route: entity missing after merge", {
|
|
7549
9346
|
method: m,
|
|
7550
|
-
pathJoined:
|
|
9347
|
+
pathJoined: path2.join("/"),
|
|
7551
9348
|
entityMapKeyCount: Object.keys(entityMap).length
|
|
7552
9349
|
});
|
|
7553
9350
|
return config.json(
|
|
@@ -7555,19 +9352,19 @@ function createCmsApiHandler(config) {
|
|
|
7555
9352
|
{ status: 503 }
|
|
7556
9353
|
);
|
|
7557
9354
|
}
|
|
7558
|
-
if (
|
|
9355
|
+
if (path2.length === 1) {
|
|
7559
9356
|
if (m === "GET") return crud.GET(req, "llm_agents");
|
|
7560
9357
|
if (m === "POST") return crud.POST(req, "llm_agents");
|
|
7561
9358
|
}
|
|
7562
|
-
if (
|
|
7563
|
-
const id =
|
|
9359
|
+
if (path2.length === 2 && !path2[1]?.includes("knowledge")) {
|
|
9360
|
+
const id = path2[1];
|
|
7564
9361
|
if (m === "GET") return crudById.GET(req, "llm_agents", id);
|
|
7565
9362
|
if (m === "PUT" || m === "PATCH") return crudById.PUT(req, "llm_agents", id);
|
|
7566
9363
|
if (m === "DELETE") return crudById.DELETE(req, "llm_agents", id);
|
|
7567
9364
|
}
|
|
7568
9365
|
}
|
|
7569
9366
|
{
|
|
7570
|
-
const kbMatch = matchLlmAgentKnowledgeRoute(
|
|
9367
|
+
const kbMatch = matchLlmAgentKnowledgeRoute(path2);
|
|
7571
9368
|
if (kbMatch) {
|
|
7572
9369
|
if (!llmAgentKnowledgeHandlers) {
|
|
7573
9370
|
return config.json(
|
|
@@ -7590,29 +9387,29 @@ function createCmsApiHandler(config) {
|
|
|
7590
9387
|
}
|
|
7591
9388
|
}
|
|
7592
9389
|
}
|
|
7593
|
-
if (
|
|
7594
|
-
if (
|
|
7595
|
-
if (
|
|
7596
|
-
if (
|
|
7597
|
-
if (
|
|
9390
|
+
if (path2[0] === "chat" && chatHandlers) {
|
|
9391
|
+
if (path2.length === 2 && path2[1] === "config" && m === "GET") return chatHandlers.publicConfig(req);
|
|
9392
|
+
if (path2.length === 2 && path2[1] === "identify" && m === "POST") return chatHandlers.identify(req);
|
|
9393
|
+
if (path2.length === 4 && path2[1] === "conversations" && path2[3] === "messages" && m === "GET") return chatHandlers.getMessages(req, path2[2]);
|
|
9394
|
+
if (path2.length === 2 && path2[1] === "messages" && m === "POST") return chatHandlers.postMessage(req);
|
|
7598
9395
|
}
|
|
7599
|
-
if (
|
|
9396
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "invoice" && m === "GET" && getCms) {
|
|
7600
9397
|
const a = await config.requireAuth(req);
|
|
7601
9398
|
if (a) return a;
|
|
7602
9399
|
const pe = await requireEntityPermissionEffective(req, "orders", "read");
|
|
7603
9400
|
if (pe) return pe;
|
|
7604
9401
|
const cms = await getCms();
|
|
7605
9402
|
const { streamOrderInvoicePdf: streamOrderInvoicePdf2 } = await Promise.resolve().then(() => (init_erp_order_invoice(), erp_order_invoice_exports));
|
|
7606
|
-
const oid = Number(
|
|
9403
|
+
const oid = Number(path2[1]);
|
|
7607
9404
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
7608
9405
|
return streamOrderInvoicePdf2(cms, dataSource, entityMap, oid, {});
|
|
7609
9406
|
}
|
|
7610
|
-
if (
|
|
9407
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "repost-erp" && getCms) {
|
|
7611
9408
|
const a = await config.requireAuth(req);
|
|
7612
9409
|
if (a) return a;
|
|
7613
9410
|
const pe = await requireEntityPermissionEffective(req, "orders", m === "GET" ? "read" : "update");
|
|
7614
9411
|
if (pe) return pe;
|
|
7615
|
-
const oid = Number(
|
|
9412
|
+
const oid = Number(path2[1]);
|
|
7616
9413
|
if (!Number.isFinite(oid)) return config.json({ error: "Invalid id" }, { status: 400 });
|
|
7617
9414
|
const cms = await getCms();
|
|
7618
9415
|
const { isErpIntegrationEnabled: isErpIntegrationEnabled3 } = await Promise.resolve().then(() => (init_erp_config_enabled(), erp_config_enabled_exports));
|
|
@@ -7628,13 +9425,13 @@ function createCmsApiHandler(config) {
|
|
|
7628
9425
|
}
|
|
7629
9426
|
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
7630
9427
|
}
|
|
7631
|
-
if (
|
|
7632
|
-
const resource = resolveResource(
|
|
9428
|
+
if (path2.length === 0) return config.json({ error: "Not found" }, { status: 404 });
|
|
9429
|
+
const resource = resolveResource(path2[0]);
|
|
7633
9430
|
if (!crudResources.includes(resource)) {
|
|
7634
9431
|
console.warn(CMS_API_LOG, "generic CRUD gate: Invalid resource (not in crudResources)", {
|
|
7635
9432
|
method: m,
|
|
7636
|
-
pathJoined:
|
|
7637
|
-
pathSegments:
|
|
9433
|
+
pathJoined: path2.join("/"),
|
|
9434
|
+
pathSegments: path2,
|
|
7638
9435
|
resource,
|
|
7639
9436
|
hasLlmAgentsEntity: Boolean(entityMap.llm_agents),
|
|
7640
9437
|
crudResourcesHasResource: crudResources.includes(resource),
|
|
@@ -7642,24 +9439,24 @@ function createCmsApiHandler(config) {
|
|
|
7642
9439
|
});
|
|
7643
9440
|
return config.json({ error: "Invalid resource" }, { status: 400 });
|
|
7644
9441
|
}
|
|
7645
|
-
if (
|
|
7646
|
-
if (
|
|
9442
|
+
if (path2.length === 2) {
|
|
9443
|
+
if (path2[1] === "metadata" && m === "GET") {
|
|
7647
9444
|
return crud.GET_METADATA(req, resource);
|
|
7648
9445
|
}
|
|
7649
|
-
if (
|
|
9446
|
+
if (path2[1] === "bulk" && m === "POST") {
|
|
7650
9447
|
return crud.BULK_POST(req, resource);
|
|
7651
9448
|
}
|
|
7652
|
-
if (
|
|
9449
|
+
if (path2[1] === "export" && m === "GET") {
|
|
7653
9450
|
return crud.GET_EXPORT(req, resource);
|
|
7654
9451
|
}
|
|
7655
9452
|
}
|
|
7656
|
-
if (
|
|
9453
|
+
if (path2.length === 1) {
|
|
7657
9454
|
if (m === "GET") return crud.GET(req, resource);
|
|
7658
9455
|
if (m === "POST") return crud.POST(req, resource);
|
|
7659
9456
|
return config.json({ error: "Method not allowed" }, { status: 405 });
|
|
7660
9457
|
}
|
|
7661
|
-
if (
|
|
7662
|
-
const id =
|
|
9458
|
+
if (path2.length === 2) {
|
|
9459
|
+
const id = path2[1];
|
|
7663
9460
|
if (m === "GET") return crudById.GET(req, resource, id);
|
|
7664
9461
|
if (m === "PUT" || m === "PATCH") return crudById.PUT(req, resource, id);
|
|
7665
9462
|
if (m === "DELETE") return crudById.DELETE(req, resource, id);
|
|
@@ -8377,7 +10174,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8377
10174
|
};
|
|
8378
10175
|
}
|
|
8379
10176
|
return {
|
|
8380
|
-
async handle(method,
|
|
10177
|
+
async handle(method, path2, req) {
|
|
8381
10178
|
try {
|
|
8382
10179
|
let serializeAddress2 = function(a) {
|
|
8383
10180
|
return {
|
|
@@ -8393,7 +10190,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8393
10190
|
};
|
|
8394
10191
|
};
|
|
8395
10192
|
var serializeAddress = serializeAddress2;
|
|
8396
|
-
if (
|
|
10193
|
+
if (path2[0] === "products" && path2.length === 1 && method === "GET") {
|
|
8397
10194
|
const url = new URL(req.url || "", "http://localhost");
|
|
8398
10195
|
const collectionSlug = url.searchParams.get("collection")?.trim();
|
|
8399
10196
|
const collectionId = url.searchParams.get("collectionId");
|
|
@@ -8435,8 +10232,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8435
10232
|
...collectionFilter && { collection: collectionFilter }
|
|
8436
10233
|
});
|
|
8437
10234
|
}
|
|
8438
|
-
if (
|
|
8439
|
-
const idOrSlug =
|
|
10235
|
+
if (path2[0] === "products" && path2.length === 2 && method === "GET") {
|
|
10236
|
+
const idOrSlug = path2[1];
|
|
8440
10237
|
const byId = /^\d+$/.test(idOrSlug);
|
|
8441
10238
|
const product = await productRepo().findOne({
|
|
8442
10239
|
where: byId ? { id: parseInt(idOrSlug, 10), status: "available", deleted: false } : { slug: idOrSlug, status: "available", deleted: false },
|
|
@@ -8451,7 +10248,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8451
10248
|
})).filter((t) => t.name || t.value);
|
|
8452
10249
|
return json({ ...serializeProduct(p), attributes: attributeTags });
|
|
8453
10250
|
}
|
|
8454
|
-
if (
|
|
10251
|
+
if (path2[0] === "collections" && path2.length === 1 && method === "GET") {
|
|
8455
10252
|
const items = await collectionRepo().find({
|
|
8456
10253
|
where: { active: true, deleted: false },
|
|
8457
10254
|
order: { sortOrder: "ASC", id: "ASC" }
|
|
@@ -8480,8 +10277,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8480
10277
|
})
|
|
8481
10278
|
});
|
|
8482
10279
|
}
|
|
8483
|
-
if (
|
|
8484
|
-
const idOrSlug =
|
|
10280
|
+
if (path2[0] === "collections" && path2.length === 2 && method === "GET") {
|
|
10281
|
+
const idOrSlug = path2[1];
|
|
8485
10282
|
const byId = /^\d+$/.test(idOrSlug);
|
|
8486
10283
|
const collection = await collectionRepo().findOne({
|
|
8487
10284
|
where: byId ? { id: parseInt(idOrSlug, 10), active: true, deleted: false } : { slug: idOrSlug, active: true, deleted: false },
|
|
@@ -8504,7 +10301,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8504
10301
|
products: products.map((p) => serializeProduct(p))
|
|
8505
10302
|
});
|
|
8506
10303
|
}
|
|
8507
|
-
if (
|
|
10304
|
+
if (path2[0] === "profile" && path2.length === 1 && method === "GET") {
|
|
8508
10305
|
const u = await getSessionUser();
|
|
8509
10306
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8510
10307
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -8521,7 +10318,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8521
10318
|
} : null
|
|
8522
10319
|
});
|
|
8523
10320
|
}
|
|
8524
|
-
if (
|
|
10321
|
+
if (path2[0] === "profile" && path2.length === 1 && method === "PUT") {
|
|
8525
10322
|
const u = await getSessionUser();
|
|
8526
10323
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8527
10324
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -8558,7 +10355,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8558
10355
|
if (!contact) return json({ error: "Contact not found" }, { status: 404 });
|
|
8559
10356
|
return { contactId: contact.id };
|
|
8560
10357
|
}
|
|
8561
|
-
if (
|
|
10358
|
+
if (path2[0] === "addresses" && path2.length === 1 && method === "GET") {
|
|
8562
10359
|
const contactOrErr = await getContactForAddresses();
|
|
8563
10360
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8564
10361
|
const list = await addressRepo().find({
|
|
@@ -8567,7 +10364,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8567
10364
|
});
|
|
8568
10365
|
return json({ addresses: list.map((a) => serializeAddress2(a)) });
|
|
8569
10366
|
}
|
|
8570
|
-
if (
|
|
10367
|
+
if (path2[0] === "addresses" && path2.length === 1 && method === "POST") {
|
|
8571
10368
|
const contactOrErr = await getContactForAddresses();
|
|
8572
10369
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8573
10370
|
const b = await req.json().catch(() => ({}));
|
|
@@ -8586,10 +10383,10 @@ function createStorefrontApiHandler(config) {
|
|
|
8586
10383
|
const created = await addressRepo().save(addressRepo().create(row));
|
|
8587
10384
|
return json(serializeAddress2(created));
|
|
8588
10385
|
}
|
|
8589
|
-
if (
|
|
10386
|
+
if (path2[0] === "addresses" && path2.length === 2 && (method === "PATCH" || method === "PUT")) {
|
|
8590
10387
|
const contactOrErr = await getContactForAddresses();
|
|
8591
10388
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8592
|
-
const id = parseInt(
|
|
10389
|
+
const id = parseInt(path2[1], 10);
|
|
8593
10390
|
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
8594
10391
|
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
8595
10392
|
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
@@ -8615,17 +10412,17 @@ function createStorefrontApiHandler(config) {
|
|
|
8615
10412
|
const updated = await addressRepo().findOne({ where: { id } });
|
|
8616
10413
|
return json(serializeAddress2(updated));
|
|
8617
10414
|
}
|
|
8618
|
-
if (
|
|
10415
|
+
if (path2[0] === "addresses" && path2.length === 2 && method === "DELETE") {
|
|
8619
10416
|
const contactOrErr = await getContactForAddresses();
|
|
8620
10417
|
if (contactOrErr instanceof Response) return contactOrErr;
|
|
8621
|
-
const id = parseInt(
|
|
10418
|
+
const id = parseInt(path2[1], 10);
|
|
8622
10419
|
if (!Number.isFinite(id)) return json({ error: "Invalid id" }, { status: 400 });
|
|
8623
10420
|
const existing = await addressRepo().findOne({ where: { id, contactId: contactOrErr.contactId } });
|
|
8624
10421
|
if (!existing) return json({ error: "Not found" }, { status: 404 });
|
|
8625
10422
|
await addressRepo().delete(id);
|
|
8626
10423
|
return json({ deleted: true });
|
|
8627
10424
|
}
|
|
8628
|
-
if (
|
|
10425
|
+
if (path2[0] === "verify-email" && path2.length === 1 && method === "POST") {
|
|
8629
10426
|
const b = await req.json().catch(() => ({}));
|
|
8630
10427
|
const token = typeof b.token === "string" ? b.token.trim() : "";
|
|
8631
10428
|
if (!token) return json({ error: "token is required" }, { status: 400 });
|
|
@@ -8644,7 +10441,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8644
10441
|
await tokenRepo().delete({ email });
|
|
8645
10442
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
8646
10443
|
}
|
|
8647
|
-
if (
|
|
10444
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "send" && path2.length === 3 && method === "POST") {
|
|
8648
10445
|
const b = await req.json().catch(() => ({}));
|
|
8649
10446
|
const purposeRaw = typeof b.purpose === "string" ? b.purpose.trim() : "";
|
|
8650
10447
|
const purpose = purposeRaw === "login" || purposeRaw === "verify_email" || purposeRaw === "verify_phone" ? purposeRaw : "";
|
|
@@ -8729,7 +10526,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8729
10526
|
}
|
|
8730
10527
|
return json({ ok: true });
|
|
8731
10528
|
}
|
|
8732
|
-
if (
|
|
10529
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "verify-email" && path2.length === 3 && method === "POST") {
|
|
8733
10530
|
if (otpOff("verifyEmail")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
8734
10531
|
const b = await req.json().catch(() => ({}));
|
|
8735
10532
|
const email = typeof b.email === "string" ? b.email.trim().toLowerCase() : "";
|
|
@@ -8752,7 +10549,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8752
10549
|
await tokenRepo().delete({ email });
|
|
8753
10550
|
return json({ success: true, message: "Email verified. You can sign in." });
|
|
8754
10551
|
}
|
|
8755
|
-
if (
|
|
10552
|
+
if (path2[0] === "auth" && path2[1] === "otp" && path2[2] === "verify-phone" && path2.length === 3 && method === "POST") {
|
|
8756
10553
|
if (otpOff("verifyPhone")) return json({ error: "otp_disabled" }, { status: 403 });
|
|
8757
10554
|
const su = await getSessionUser();
|
|
8758
10555
|
const uid = su?.id ? parseInt(String(su.id), 10) : NaN;
|
|
@@ -8780,7 +10577,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8780
10577
|
}
|
|
8781
10578
|
return json({ success: true });
|
|
8782
10579
|
}
|
|
8783
|
-
if (
|
|
10580
|
+
if (path2[0] === "register" && path2.length === 1 && method === "POST") {
|
|
8784
10581
|
if (!config.hashPassword) return json({ error: "Registration not configured" }, { status: 501 });
|
|
8785
10582
|
const b = await req.json().catch(() => ({}));
|
|
8786
10583
|
const capReg = await assertCaptchaOk(getCms, b, req, json);
|
|
@@ -8837,14 +10634,14 @@ function createStorefrontApiHandler(config) {
|
|
|
8837
10634
|
emailVerificationSent
|
|
8838
10635
|
});
|
|
8839
10636
|
}
|
|
8840
|
-
if (
|
|
10637
|
+
if (path2[0] === "cart" && path2.length === 1 && method === "GET") {
|
|
8841
10638
|
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
8842
10639
|
if (err) return err;
|
|
8843
10640
|
const body = serializeCart(cart);
|
|
8844
10641
|
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
8845
10642
|
return json(body);
|
|
8846
10643
|
}
|
|
8847
|
-
if (
|
|
10644
|
+
if (path2[0] === "cart" && path2[1] === "items" && path2.length === 2 && method === "POST") {
|
|
8848
10645
|
const body = await req.json().catch(() => ({}));
|
|
8849
10646
|
const capCart = await assertCaptchaOk(getCms, body, req, json);
|
|
8850
10647
|
if (capCart) return capCart;
|
|
@@ -8875,8 +10672,8 @@ function createStorefrontApiHandler(config) {
|
|
|
8875
10672
|
if (setCookie) return json(out, { headers: { "Set-Cookie": setCookie } });
|
|
8876
10673
|
return json(out);
|
|
8877
10674
|
}
|
|
8878
|
-
if (
|
|
8879
|
-
const itemId = parseInt(
|
|
10675
|
+
if (path2[0] === "cart" && path2[1] === "items" && path2.length === 3) {
|
|
10676
|
+
const itemId = parseInt(path2[2], 10);
|
|
8880
10677
|
if (!Number.isFinite(itemId)) return json({ error: "Invalid item id" }, { status: 400 });
|
|
8881
10678
|
const { cart, setCookie, err } = await getOrCreateCart(req);
|
|
8882
10679
|
if (err) return err;
|
|
@@ -8909,7 +10706,7 @@ function createStorefrontApiHandler(config) {
|
|
|
8909
10706
|
return json(out);
|
|
8910
10707
|
}
|
|
8911
10708
|
}
|
|
8912
|
-
if (
|
|
10709
|
+
if (path2[0] === "cart" && path2[1] === "merge" && method === "POST") {
|
|
8913
10710
|
const u = await getSessionUser();
|
|
8914
10711
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
8915
10712
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -9015,7 +10812,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9015
10812
|
}
|
|
9016
10813
|
return { wishlist: w, setCookie: null, err: null };
|
|
9017
10814
|
}
|
|
9018
|
-
if (
|
|
10815
|
+
if (path2[0] === "wishlist" && path2.length === 1 && method === "GET") {
|
|
9019
10816
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9020
10817
|
if (err) return err;
|
|
9021
10818
|
const items = await wishlistItemRepo().find({
|
|
@@ -9043,7 +10840,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9043
10840
|
if (setCookie) return json(body, { headers: { "Set-Cookie": setCookie } });
|
|
9044
10841
|
return json(body);
|
|
9045
10842
|
}
|
|
9046
|
-
if (
|
|
10843
|
+
if (path2[0] === "wishlist" && path2[1] === "items" && path2.length === 2 && method === "POST") {
|
|
9047
10844
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9048
10845
|
if (err) return err;
|
|
9049
10846
|
const b = await req.json().catch(() => ({}));
|
|
@@ -9057,15 +10854,15 @@ function createStorefrontApiHandler(config) {
|
|
|
9057
10854
|
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
9058
10855
|
return json({ ok: true });
|
|
9059
10856
|
}
|
|
9060
|
-
if (
|
|
10857
|
+
if (path2[0] === "wishlist" && path2[1] === "items" && path2.length === 3 && method === "DELETE") {
|
|
9061
10858
|
const { wishlist, setCookie, err } = await getOrCreateWishlist(req);
|
|
9062
10859
|
if (err) return err;
|
|
9063
|
-
const productId = parseInt(
|
|
10860
|
+
const productId = parseInt(path2[2], 10);
|
|
9064
10861
|
await wishlistItemRepo().delete({ wishlistId: wishlist.id, productId });
|
|
9065
10862
|
if (setCookie) return json({ ok: true }, { headers: { "Set-Cookie": setCookie } });
|
|
9066
10863
|
return json({ ok: true });
|
|
9067
10864
|
}
|
|
9068
|
-
if (
|
|
10865
|
+
if (path2[0] === "checkout" && path2[1] === "order" && path2.length === 2 && method === "POST") {
|
|
9069
10866
|
const b = await req.json().catch(() => ({}));
|
|
9070
10867
|
const capOrd = await assertCaptchaOk(getCms, b, req, json);
|
|
9071
10868
|
if (capOrd) return capOrd;
|
|
@@ -9168,7 +10965,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9168
10965
|
currency: cart.currency || "INR"
|
|
9169
10966
|
});
|
|
9170
10967
|
}
|
|
9171
|
-
if (
|
|
10968
|
+
if (path2[0] === "checkout" && path2.length === 1 && method === "POST") {
|
|
9172
10969
|
const b = await req.json().catch(() => ({}));
|
|
9173
10970
|
const capChk = await assertCaptchaOk(getCms, b, req, json);
|
|
9174
10971
|
if (capChk) return capChk;
|
|
@@ -9270,7 +11067,7 @@ function createStorefrontApiHandler(config) {
|
|
|
9270
11067
|
total: prepChk.orderTotal
|
|
9271
11068
|
});
|
|
9272
11069
|
}
|
|
9273
|
-
if (
|
|
11070
|
+
if (path2[0] === "orders" && path2.length === 1 && method === "GET") {
|
|
9274
11071
|
const u = await getSessionUser();
|
|
9275
11072
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9276
11073
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
@@ -9312,27 +11109,27 @@ function createStorefrontApiHandler(config) {
|
|
|
9312
11109
|
})
|
|
9313
11110
|
});
|
|
9314
11111
|
}
|
|
9315
|
-
if (
|
|
11112
|
+
if (path2[0] === "orders" && path2.length === 3 && path2[2] === "invoice" && method === "GET") {
|
|
9316
11113
|
const u = await getSessionUser();
|
|
9317
11114
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9318
11115
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
9319
11116
|
if (!getCms) return json({ error: "Not found" }, { status: 404 });
|
|
9320
11117
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
9321
11118
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
9322
|
-
const orderId = parseInt(
|
|
11119
|
+
const orderId = parseInt(path2[1], 10);
|
|
9323
11120
|
if (!Number.isFinite(orderId)) return json({ error: "Invalid id" }, { status: 400 });
|
|
9324
11121
|
const cms = await getCms();
|
|
9325
11122
|
return streamOrderInvoicePdf(cms, dataSource, entityMap, orderId, {
|
|
9326
11123
|
ownerContactId: contact.id
|
|
9327
11124
|
});
|
|
9328
11125
|
}
|
|
9329
|
-
if (
|
|
11126
|
+
if (path2[0] === "orders" && path2.length === 2 && method === "GET") {
|
|
9330
11127
|
const u = await getSessionUser();
|
|
9331
11128
|
const uid = u?.id ? parseInt(String(u.id), 10) : NaN;
|
|
9332
11129
|
if (!Number.isFinite(uid)) return json({ error: "Unauthorized" }, { status: 401 });
|
|
9333
11130
|
const contact = await contactRepo().findOne({ where: { userId: uid, deleted: false } });
|
|
9334
11131
|
if (!contact) return json({ error: "Not found" }, { status: 404 });
|
|
9335
|
-
const orderId = parseInt(
|
|
11132
|
+
const orderId = parseInt(path2[1], 10);
|
|
9336
11133
|
let order = await orderRepo().findOne({
|
|
9337
11134
|
where: { id: orderId, contactId: contact.id, deleted: false },
|
|
9338
11135
|
relations: ["items", "items.product"]
|
|
@@ -9434,6 +11231,8 @@ export {
|
|
|
9434
11231
|
getPublicSettingsGroup,
|
|
9435
11232
|
mergeGuardrailsIntoSystemPrompt,
|
|
9436
11233
|
parseLlmAgentValidationRules,
|
|
11234
|
+
simpleDecrypt,
|
|
11235
|
+
simpleEncrypt,
|
|
9437
11236
|
validateUserMessageAgainstAgentRules,
|
|
9438
11237
|
validateUserMessageAgainstStructuredRules
|
|
9439
11238
|
};
|