Skip to content

designer-api — code inspection

Audit date: 2026-05-17. Lens: concrete code-level issues not already covered by someli-doc/audit/designer-api/security.md and friends.

Summary

22 findings across all severity levels. The most serious are: a plaintext password fallback in the authentication handler that lets any user authenticate by sending their stored (unhashed) password; a hardcoded Polotno commercial license key present in 20+ files; an isOnProcess flag that is never reset on the async code path that does the actual image processing work in the scheduler jobs (meaning the lock sticks permanently after the first real run); a download() error handler that silently swallows failures by returning an error object instead of throw-ing, causing callers that assume the return value is a Polotno JSON to crash in unpredictable ways; unguarded variantArray[0].rgbCode accesses that throw TypeError and freeze the isOnProcess lock across all 30+ industry-clone jobs; and a server.js DB connection guard that is logically inverted, so it never logs the actual error. Recurring themes: async work inside forEach losing the isOnProcess guard, hardcoded third-party keys in many files, SQL built from req.body string fields with no sanitisation in several endpoints, and AI prompt content flowing unchecked from req.body to OpenAI calls.

Critical

C1. Hardcoded Polotno commercial license key in 20+ source files

  • File: job_post_approved.js:68, job_preprodpost_approved.js:151,240, job_schedule_template.js:79, job_schedule_organization.js:79, job_swap.js:89, job_specific_approved_post.js:68, routes/routes.js:1261,2789,2869,3760,3840,3929,5891,8186,9179,13136, and ~5 more scripts (job_content_swap_entry.js:84, job_16_30_schedule_template.js:78, job_schedule_variant.js:55, polotno_image_uploader.js:31, s3_Image_upload.js:31)
  • What: The Polotno license key FXZvloSJvAe09-bdR9iC is hardcoded as a string literal in every call to createInstance({ key: ... }) throughout the codebase — at least 20 distinct occurrences.
  • Why it matters: The key controls billing and access to the Polotno rendering service. It is committed to version control and baked into the Docker image. Anyone with image or repo read access obtains a production billing credential. security.md F-9 noted it might be hardcoded but did not confirm; the key is confirmed present.
  • Fix: Move to conf.js / process.env.POLOTNO_KEY and replace all literal occurrences with conf.POLOTNO_KEY.

C2. Plaintext password fallback in /webauthenticate bypasses bcrypt

  • File: routes/routes.js:212-219
  • What: After bcrypt.compare returns false, the handler falls through to else if (reqObj.password == result[0].password) — a strict equality comparison of the plaintext input against the stored DB value. If any account has a plaintext password in the database (legacy migration, manually created row, or a reset operation that stored plaintext), this branch authenticates the user without any hash check.
  • Why it matters: The fallback entirely undermines bcrypt. A database compromise would immediately expose every account whose password is stored in plaintext. The branch also fires when an admin sets a password directly in SQL without hashing it.
  • Fix: Remove lines 212-219 entirely. The only valid success branches are checkUser === true (bcrypt match) and the social-auth branch (line 220). If plaintext accounts exist, migrate them.

C3. isOnProcess lock permanently stuck after first real run in job_post_approved.js and job_specific_approved_post.js

  • File: job_post_approved.js:20-112, job_specific_approved_post.js:20-112
  • What: Both jobs have no isOnProcess guard at all — getTempalteStatusList() fires on every cron tick with no lock. However, within each run the tAutoDesignPost status is set to 'Added to Library' at line 111 unconditionally, outside the S3 upload callback. This means the record is marked done before the image is generated or uploaded. If Polotno or S3 fails, the record is permanently marked done with no image. More critically, instance.close() is called inside the S3 upload callback at line 94; if the upload callback errors, the instance leaks open.
  • Why it matters: Every approved post that hits an S3 or Polotno error is silently skipped with status 'Added to Library' — corrupting the pipeline permanently for that post. Polotno instances leak on S3 upload failure, consuming memory until the process is killed.
  • Fix: Move con.query(UPDATE ... attachedLibrary = 1 ...) inside the success branch of the S3 upload callback. Wrap instance creation in try/finally to guarantee instance.close().

C4. download() returns an error object on failure instead of throwing — silent data corruption

  • File: job_post_approved.js:115-131, same pattern in job_preprodpost_approved.js, job_schedule_template.js, job_specific_approved_post.js, job_schedule_organization.js, routes/routes.js (the download1 function at line ~99)
  • What: The download() and download1() functions catch errors and return { statusCode: ..., body: ... } instead of re-throwing. Callers then pass this error object to JSON.stringify(), Buffer.from(), and instance.jsonToImageBase64() as if it were a valid Polotno JSON document.
  • Why it matters: When S3 fetch fails (e.g., key not found, network error), the error object { statusCode: 400, body: "..." } is silently serialised as JSON and uploaded to S3 as a template, and then fed to instance.jsonToImageBase64() which throws because it is not a Polotno document. The thrown error propagates out of the async callback and becomes an unhandled promise rejection. The record is still marked done.
  • Fix: Replace return { statusCode: ... } with throw err in all download() catch blocks. Each call site already has its own try/catch.

High

H1. Google OAuth client secret committed in conf/credentials.json

  • File: conf/credentials.json:8
  • What: The file contains a live Google OAuth 2.0 client secret (drGPpS_h7PCgekNpN5P3Fr5c) for project someli-web. The file is tracked by git (it appears in the file listing from the repo, not in .gitignore).
  • Why it matters: Anyone with repo read access can impersonate the OAuth application, issue forged authorization codes, and redirect Google auth flows. security.md F-6 said "verify whether committed" — it is committed.
  • Fix: Rotate the client secret in Google Cloud Console immediately. Add conf/credentials.json to .gitignore. Load client credentials from environment variables or Secret Manager.

H2. commonDelete and commonIsDeleted endpoints perform arbitrary table mutations with no auth check

  • File: routes/routes.js:303-320 (/commonDelete), 368-386 (/commonIsDeleted)
  • What: Both endpoints take req.body.table and req.body.id and execute DELETE FROM <table> WHERE id=<id> / UPDATE <table> SET isdeleted=1 WHERE id=<id> with no authentication check whatsoever. Any unauthenticated caller can delete or soft-delete any row in any table by specifying the table name and an id.
  • Why it matters: Full unauthenticated delete access to the entire database schema. An attacker who discovers these endpoints can destroy all library content, templates, users, or jobs.
  • Fix: Add an auth check (token validation against tMember.auth_token) before the query executes. These endpoints should be removed and replaced with specific typed endpoints that operate only on allowed tables.

H3. commonPagecount accepts arbitrary table name and WHERE clause from req.body

  • File: routes/routes.js:323-340
  • What: SELECT COUNT(id) AS rowcount FROM ${reqObj.table} WHERE ${reqObj.condition} — both the table name and the entire WHERE condition are taken directly from req.body with no authentication check and no sanitisation.
  • Why it matters: This is a full unauthenticated arbitrary read from any table with any condition. It can be used to enumerate data across all tables (time-based or boolean-based extraction of any column by constructing conditions like id=1 AND SUBSTRING(password,1,1)='a').
  • Fix: Auth check + restrict to an allowlist of table names and structured condition parameters.

H4. Unguarded variantArray[0] access causes TypeError in all industry-clone jobs

  • File: job_disability_insurance.js:47, job_motor_insurance.js:47, job_travel_insurance.js:47, and the same line in all ~30 industry-clone jobs
  • What: con.query(... ORDER BY RAND() LIMIT 1) may return an empty array if all variants are already used for the day. The next line unconditionally accesses variantArray[0].rgbCode without checking length. This throws TypeError: Cannot read property 'rgbCode' of undefined.
  • Why it matters: In job_disability_insurance.js and most clones, the isOnProcess flag is set to false only at the end of the industryData.forEach loop — but forEach with async callbacks does not await them. The thrown TypeError propagates out of the synchronous forEach callback; the outer function catches nothing. The real impact is a noisy unhandled rejection per callback invocation and the entire template insertion step being skipped silently for that category.
  • Why it's distinct from known F-8: F-8 documents the general async/forEach pattern; this is a specific, concrete TypeError caused by an unguarded array access that happens when the variant pool is exhausted — a routine operational condition.
  • Fix: Add if (!variantArray || variantArray.length === 0) continue; (or return inside the forEach callback) before accessing variantArray[0].

H5. SQL injection via req.body.text, req.body.category, req.body.searchTerm in image search endpoints

  • File: routes/routes.js:1499,1580,1608,1620,1703,1705,1810,1812,1828,1845,1847
  • What: Multiple endpoints (/unsplashsearchimages, /pixabaysearchimages, /pexelssearchimages, /searchnewimages) interpolate req.body.text, req.body.category, req.body.searchTerm, req.body.tableName, and req.body.trowId directly into SQL strings sent to the synchronous con.query() (which is sync-mysql). For example: `SELECT * FROM tMediaLib WHERE searchTerm LIKE '${req.body.searchTerm}%' AND category ="${req.body.category}"`
  • Why it matters: A caller can inject arbitrary SQL. Because there is no auth middleware (F-3), these endpoints are reachable unauthenticated. The combination gives full unauthenticated SQL injection.
  • Fix: Use parameterised queries via mysql2 (which is already imported) for all of these endpoints.

H6. checkUserPassword leaks the plaintext password in the query and the response

  • File: routes/routes.js:435,442
  • What: The endpoint queries WHERE tMemberAuth.sid='${email}' AND tMember.password='${password}' — comparing the supplied plaintext password against the stored hash as a string equality, which always fails for bcrypt hashes. Beyond being broken, it logs the password as part of the SQL string in any error path. The response at line 446 returns responseObj.data = responseData where responseData is JSON.parse(JSON.stringify(result[0])) — the full tMember row including the password column.
  • Why it matters: The hashed password of the user is returned to the caller in the response body. Any caller who can reach this endpoint receives the bcrypt hash, which is useful for offline cracking.
  • Fix: Select only needed columns. Use bcrypt for comparison. Never return the password field.

H7. generateTextContent.js error handler crashes and masks the real error

  • File: generateTextContent.js:58
  • What: The catch block does console.log("ERROR : ", error.response.data.error). If error.response is null (e.g., network timeout, DNS error), this line itself throws TypeError: Cannot read properties of undefined, masking the original error. isOnProcess is then never reset because line 65 (isOnProcess = false) is in the normal flow and is skipped.
  • Why it matters: A network error to OpenAI permanently locks the job — no more content generation until the process restarts. The actual error is invisible.
  • Fix: Use error?.response?.data?.error ?? error.message and always place isOnProcess = false in a finally block.

Medium

M1. job_schedule_template.js: status set to 'Completedd' (typo) and race with setTimeout

  • File: job_schedule_template.js:110-113
  • What: Two con.query(UPDATE tTempalte_Status) calls are made: one inside a setTimeout(..., 10000) setting status to 'Completed', and one immediately after the setTimeout registration (line 113) setting status to 'Completedd' (double-d typo). Because setTimeout is non-blocking, the immediate call at line 113 fires first — so the record is immediately set to 'Completedd' before any processing occurs. The callback inside setTimeout then overwrites it with 'Completed' 10 seconds later if successful, but the getTempalteStatusList query at line 37 filters for tStatus = 'Inserted' — so the 'Completedd' status is never picked up by any consumer.
  • Why it matters: Every record processed by job_schedule_template.js ends up with status 'Completedd' for ~10 seconds, then 'Completed'. If the process restarts in that window, records are permanently left in 'Completedd'. job_schedule_organization.js:111 has the same pattern. Affected records can never be reprocessed.
  • Fix: Remove the immediate con.query at line 113. Set status to 'Completed' only inside the S3 upload success callback. Same fix applies to job_schedule_organization.js:111.

M2. job_schedule_dynamic.js missing isOnProcess = false in the outer scope

  • File: job_schedule_dynamic.js:17-63
  • What: isOnProcess is set to true at line 19, but there is no isOnProcess = false in the outer async function after the industryData.forEach loop completes (only a log message at line 62). The isOnProcess flag is never reset to false, so the job runs exactly once and never again.
  • Why it matters: The dynamic post schedule runs once per process lifetime, not hourly as intended.
  • Fix: Add isOnProcess = false after the closing brace of the outer if(industryData ...) block and in a catch handler.

M3. job_gemini_Images.js: isOnProcess set after forEach, not awaited — job never actually locks

  • File: job_gemini_Images.js:36-46
  • What: The isOnProcess = true assignment at line 39 is inside the if(getQuery...) block but after firing getQuery.forEach(async(q) => { await gemini(q) }). The forEach returns immediately (not awaited). isOnProcess = false is then set at line 45 before any gemini() call completes. So on the next tick, isOnProcess is already false and the next cron tick starts another batch of up to 5 parallel Gemini calls, creating unbounded concurrency against the Gemini API.
  • Why it matters: Every cron tick (every minute) launches up to 5 more Gemini API calls even if the previous 5 haven't finished. Under load this fans out into dozens of concurrent AI API calls, causing runaway billing.
  • Fix: Use for (const q of getQuery) { await gemini(q); } and move isOnProcess = false to a finally block.

M4. server.js DB connection error handler is logically inverted — DB errors are silently ignored

  • File: server.js:26
  • What: db.connect(conf, (res) => { if (!res && !res.status) { console.log("Something Went Wrong!") } }). The condition is !res && !res.status — this requires res to be falsy AND res.status to be falsy simultaneously. If res is falsy (null/undefined), the second operand !res.status causes a TypeError: Cannot read property 'status' of null/undefined. The condition is never actually true for a failed connection: the callback receives { status: false, data: "Not Connected!" } on failure, so !res is false and the branch is never entered. DB connection failures produce no log message.
  • Why it matters: If the DB is down on startup, the server starts anyway and all requests silently fail. No operator alert.
  • Fix: if (!res || !res.status) { console.error("DB connection failed:", res); process.exit(1); }.

M5. helper/index.js: cropUpload uploads the raw input buffer as the "full" image rather than a processed JPEG

  • File: helper/index.js:40-44
  • What: helper/index.js imports {con, s3} from ../routes/routes (line 3), where con is a sync-mysql instance. Then at line 40: let getCodeQuery = con.query(...) — but sync-mysql's query is synchronous, so this does return the result. However, cropUpload is called with inputImagePath as the binary image buffer but then passes the same buffer as Body to s3.upload for the full image (line 59), meaning the same buffer is uploaded to both thumbs/ and images/ paths. The thumbnail (line 46-53) is the resized JPEG, but the "full" image (line 56-68) is the original unresized buffer — this is likely intentional for the full image but the code comment on line 59 says Body: inputImagePath which is the already-consumed buffer reference, not a copy.
  • Why it matters: The full-image S3 key receives the raw binary input (which may be a URL buffer, not a JPEG), not the processed full-resolution JPEG. The image at the images/ S3 key will be corrupted or raw binary.
  • Fix: Sharp .toBuffer() should be used for both the thumbnail and the full image, with the full image using .jpeg({quality:100}) without .resize().

M6. content_generation_bot.js: AI output inserted directly into SQL via string interpolation

  • File: content_generation_bot.js:221
  • What: INSERT INTO tCopies(topicId,categoryId, caption, header, title, body) VALUES (${topicid}, ${categoryId}, "${q.Caption}", "${q.Header}", "${q.Title}", "${q.Body}") — the fields q.Caption, q.Header, q.Title, q.Body are LLM output inserted with double-quote string interpolation. LLM output containing double quotes or SQL metacharacters breaks the query or enables SQL injection.
  • Why it matters: AI-generated content commonly contains apostrophes, double quotes, and special characters. A crafted prompt or a model output containing " followed by SQL terminates the string and injects arbitrary SQL into the tCopies table.
  • Fix: Use parameterised queries. The same function already shows parameterised usage in FAQsbot.js:135-138 — use that pattern consistently.

M7. FAQsbot.js: debug console.log reveals raw interpolated SQL

  • File: FAQsbot.js:134
  • What: console.log("SQL : ", \INSERT INTO tCopies SET topicId=1,caption="${q.Caption}",header="${q.Header}",title="${q.Title}",body="${q.Body}") — while the actual INSERT at lines 135-138 is parameterised, the debug log constructs the same string-interpolated SQL and prints it to stdout (which goes to PM2 logs and potentially log aggregators). If any of these fields contain PII or sensitive content, it is logged in cleartext.
  • Why it matters: AI-generated content is logged in structured SQL form. Log aggregation systems (Slack, Datadog) will receive this. Separately, if LLM output ever contains passwords or PII from scraped content, it is logged.
  • Fix: Remove the debug console.log at line 134.

M8. Hardcoded Pixabay API key in multiple files alongside the DB-stored key lookup

  • File: job_schedule_template.js:353, job_schedule_organization.js:302, routes/routes.js:1408,1457
  • What: Despite a getkey("pixabay") function that fetches the active key from tApiKeys, several places hardcode key=40661313-6e6f015308502e262c449b0ec directly in the Pixabay API URL. The getkey() result is fetched but then ignored because the URL string uses the hardcoded key.
  • Why it matters: The hardcoded key bypasses the key rotation/error-fallback mechanism. When this key hits rate limits or is revoked, the fallback to the DB-stored replacement key never happens for these calls.
  • Fix: Use ${pixabay_key} (already obtained via getkey()) in the URL instead of the hardcoded value.

Low / nits

L1. routes/routes.js: titleCase() function is broken — references undefined splitStr

  • File: routes/routes.js:76-78
  • What: function titleCase(str) { return splitStr.join(' '); }splitStr is never defined in this function. The function throws a ReferenceError on every call.
  • Fix: Either implement it properly (const splitStr = str.split(' ').map(...)) or delete it — it does not appear to be called anywhere in the routes.

L2. server.js: App.socket is always null at route init time

  • File: server.js:57-60
  • What: var socketConnection = null; ... App.socket = socketConnectionsocketConnection is captured as null before any socket has connected. Even after a client connects and socketConnection = socket is assigned, App.socket still holds null because it was assigned by value.
  • Why it matters: Any route handler that uses this.socket to emit events gets null and throws.
  • Fix: Either pass io itself as App.socket and emit on io, or make App.socket a getter function.

L3. job_check_color_with_json.js: isOnProcess set to true after firing async forEach — same non-lock pattern

  • File: job_check_color_with_json.js:29-38
  • What: getQuery.forEach(async(q) => { ... }); isOnProcess = true;isOnProcess is set after the (non-awaited) async callbacks are fired. By the time any download() call starts, isOnProcess is false again (line 40). Identical pattern in job_check_media_json.js and job_check_mediaTemplates_json.js.
  • Fix: Set isOnProcess = true before the forEach, and add a finally reset.

L4. job_preprodpost_approved.js: mediaType == 2 (video) branch initialises variables but does nothing

  • File: job_preprodpost_approved.js:206-217
  • What: When mediaType == 2, the code sets pjson, pthumb, pvideo path variables but performs no uploads, no DB inserts, and no status updates. The job then falls through to isOnProcess = false without error. Video posts are silently skipped.
  • Fix: Either implement the video processing path or log a warning and set the job status to a "video not supported" state so the record is not left in limbo.

L5. job_swap.js: isOnProcess reset races with in-flight callbacks

  • File: job_swap.js:34-76
  • What: Inside check_json_media(), isOnProcess = false at line 57 fires only if getQuery is non-empty and the forEach fires. But inside the forEach callback at line 51, the con2.query success callback sets isOnProcess = false at line 57 — however the outer isOnProcess = false at line 66 fires immediately after the forEach returns (before any con2.query callback fires). So isOnProcess is reset to false immediately, but the in-flight con2.query callbacks may race with the next cron tick.
  • Fix: Use for...of await and a finally block.

L6. generateTextContent.js and FAQsbot.js call deprecated openai.createChatCompletion API

  • File: generateTextContent.js:39, FAQsbot.js:114, content_generation_bot.js:110,190, job_schedule_template.js:336
  • What: openai.createChatCompletion(...) was the v3 SDK API. The installed openai package (v4+) uses openai.chat.completions.create(...). The v3 method does not exist on the v4 client; every call throws TypeError: openai.createChatCompletion is not a function.
  • Why it matters: All content generation bots are broken at runtime. No content is generated.
  • Fix: Replace openai.createChatCompletion({model, messages}) with await openai.chat.completions.create({model, messages}) and update response access from response.data.choices[0].message.content to response.choices[0].message.content.

Cross-cutting observations

isOnProcess as a lock is structurally unreliable across the codebase. The flag is a module-level variable. It is set to true at the start of an async function, then passed through a mixture of sync forEach loops (which return before callbacks complete), setTimeout calls, and nested S3 callbacks. In most files the false reset fires before the real async work completes. The correct pattern (used only in job_tAutoDesignPost_delete_cron.js) is synchronous body + try/finally. All other jobs need the same treatment.

The download() function and its variants are copy-pasted into at least 8 separate files (job_post_approved.js, job_preprodpost_approved.js, job_specific_approved_post.js, job_schedule_template.js, job_schedule_organization.js, job_swap.js, job_content_swap_entry.js, routes/routes.js as download1), each with slight variations and some with the error-as-return-value bug (C4). A shared module would allow a single fix.

Hardcoded API keys across the board. In addition to the already-documented Slack token and Unsplash key, this inspection confirmed: Polotno license key (C1), Google OAuth client secret (H1), Pixabay key duplicated and hardcoded in URL strings bypassing the rotation mechanism (M8). Each of these was added by copy-paste during rapid development.

OpenAI SDK v3 API calls are used throughout the codebase despite v4 being installed (L6). This means every bot and generation job that calls openai.createChatCompletion silently fails at runtime. The createCompletion calls (for text-davinci-003) in routes/routes.js are also defunct because that model was retired by OpenAI in January 2024.

AI prompts accept req.body.content without sanitisation. /AISA, /ai_image, /Ai_quotes, /Ai_2030Agenda, /Ai_historicalFacts, /AiTweets, /generateTweets, /generatepov all pass raw user input directly to OpenAI prompts. There is no length cap, no content filtering, and no output validation. A caller can send arbitrarily large inputs (up to the 50 MB body limit), triggering large token counts and unexpected API costs.