Skip to content

Someli-Designer — code inspection

Audit date: 2026-05-17. Lens: concrete code-level issues not already covered by someli-doc/audit/Someli-Designer/ and friends.

Summary

Twelve findings across the Vue layer, the Polotno React sub-project, and the Vuex store. The most severe are a hardcoded Polotno API license key committed to source, a duplicate ES module import that breaks the Pexels panel at bundle time, and unsanitized LLM output rendered with v-html across six AI-content pages. Secondary patterns: a Vue 2 reactivity bypass in the updatefeed mutation (approval badges never re-render), a missing store.on("change") autosave listener that the platform fork has but the designer fork omits (work lost on crash), and window.addEventListener calls for a custom event and a scroll handler that are never removed in beforeDestroy, leaking on every page visit. Also flagged: setInterval used where setTimeout was intended in save callbacks (fires repeatedly), isLoading={true} hardcoded in three template panels (spinner never clears), and commonPagecount condition strings built by interpolating user-selected IDs (server-side SQL injection surface).

Critical

C1. Polotno license key hardcoded in committed source

  • File: polotno-editor/index.js:7
  • What: createStore({ key: "FXZvloSJvAe09-bdR9iC" }) — the Polotno license key is a string literal in committed source (same key is also in someli-platform/polotno-editor/index.js:14).
  • Why it matters: Any staff member or CI system with repo read access, or anyone who can inspect the committed polotno-bundle.js, can extract and abuse the license key to run an unlicensed Polotno deployment, potentially triggering a license violation and service suspension.
  • Fix: Move to process.env.POLOTNO_LICENSE_KEY and inject via the .env file (Parcel inlines process.env at build time). Ensure .env is in .gitignore.

C2. Duplicate import axios in Pexels.js — Pexels panel fails to build

  • File: polotno-editor/Pexels.js:4 and :10
  • What: import axios from "axios" appears on both line 4 and line 10 of the same module.
  • Why it matters: ES module semantics and Parcel both reject duplicate imports from the same specifier, meaning the bundle build fails or the Pexels panel is silently broken. Staff cannot insert Pexels stock photos into templates.
  • Fix: Remove line 10. Note that axios is not actually used in Pexels.js (Pexels API calls go through useInfiniteAPI/fetch), so the import at line 4 can be removed entirely.

High

H1. Unsanitized LLM / PDF output rendered via v-html — stored XSS risk

  • Files:
  • pages/AI-Questions.vue:36v-html="resContent" where resContent = data.data.text.replace(/\n/g, '<br>')
  • pages/AI-Content.vue:38, AI-Tips.vue:38, AI-Jokes.vue:38, AI-Myths.vue:38, AI-Mistakes.vue:38v-html="rs.content" where rs.content = element.replace(/\n/g, '<br>') + '.'
  • pages/pdfToContent.vue:36v-html="p.content" from a PDF extraction endpoint
  • What: API response text from LLM and PDF endpoints is piped directly through v-html with only a newline-to-<br> substitution. No HTML sanitization.
  • Why it matters: A prompt-injected LLM response or a malicious PDF containing <script> or <img onerror=...> executes JavaScript in the staff browser with full session access. This is an internal tool but session theft / credential exfiltration of designer staff accounts is the impact.
  • Fix: Replace v-html with {{ resContent }} and CSS white-space: pre-wrap to preserve line breaks. If rich formatting is required, sanitize with DOMPurify before binding to v-html.

H2. updatefeed mutation bypasses Vue 2 reactivity — approval badge never updates

  • File: store/index.js:125-128
  • What: state.alluserdesignlist[index].approved = item.ap directly mutates a property on an existing array element by index. Vue 2's reactivity system cannot detect this change; it requires Vue.set or array replacement.
  • Why it matters: After a staff member approves or rejects a template in Template.vue (which commits updatefeed on lines 264, 306, 329), the card background colour (driven by item.approved) does not change. The UI appears to silently ignore the action until a page reload.
  • Fix:
    updatefeed(state, item) {
      const index = state.alluserdesignlist.findIndex((x) => x.id == item.id);
      if (index !== -1) {
        Vue.set(state.alluserdesignlist, index, {
          ...state.alluserdesignlist[index],
          approved: item.ap,
        });
      }
    }
    

H3. Designer editor has no autosave — work lost on crash (fork-specific bug vs platform)

  • Files: polotno-editor/index.js (entire file) vs someli-platform/polotno-editor/index.js:17-41
  • What: The platform fork adds store.on("change", saveStateToLocalStorage) at module load, which debounces design state to localStorage every 2 s. The designer fork creates the store and calls createEditor without any change subscription. There is no loadStateFromLocalStorage call either.
  • Why it matters: A designer staff member who closes the tab, experiences a browser crash, or whose session expires loses all unsaved work on the in-progress template. The platform editor recovers silently from localStorage; the designer editor starts blank every time.
  • Fix: Port the autosave + restore pattern from someli-platform/polotno-editor/index.js:17-41 verbatim into the designer's index.js. Three additions: const href, saveStateToLocalStorage (debounced 2 s), store.on("change", saveStateToLocalStorage). Also call loadStateFromLocalStorage() inside createEditor before root.render.

H4. handleScroll defined outside methods: — scroll listener registers undefined, never removed

  • Files:
  • pages/dashboard.vue:82-103
  • pages/Template.vue:415-433
  • pages/Posts.vue:663-683
  • What: In all three pages, handleScroll() and created() are declared as top-level keys of the component options object, sibling to methods:, mounted:, etc. — not inside methods:. Vue 2 ignores unknown option keys, so this.handleScroll is undefined. created() calls window.addEventListener("scroll", this.handleScroll), which registers undefined. No beforeDestroy hook exists in any of these pages.
  • Why it matters: The scroll handler does nothing (undefined is registered), and each page mount leaks a persistent reference in window's event listener list that can never be cleaned up.
  • Fix: Move handleScroll inside the methods: object. Add beforeDestroy() { window.removeEventListener("scroll", this.handleScroll); }.

Medium

M1. get_editId_from_local_storage event listener registered without cleanup

  • Files:
  • pages/designer.vue:66
  • pages/templateEditor.vue:143
  • pages/templateEditor1.vue:150
  • pages/carousaltemplateEditor.vue:183
  • What: Each editor page calls window.addEventListener("get_editId_from_local_storage", (event) => { ... }) in mounted() with an anonymous arrow function. None have a beforeDestroy that removes it.
  • Why it matters: If the tab stays open and the component re-mounts, each mount adds another listener. When templatesPanel.js:81 dispatches the event, all accumulated listeners fire — setting tempId or editid multiple times and triggering duplicate getEditDesignList + getcolorEvent API calls.
  • Fix: Store the handler reference on the instance and remove it in beforeDestroy:
    mounted() {
      this._editIdHandler = (event) => { this.tempId = event.detail.id; ... };
      window.addEventListener("get_editId_from_local_storage", this._editIdHandler);
    },
    beforeDestroy() {
      window.removeEventListener("get_editId_from_local_storage", this._editIdHandler);
    }
    

M2. setInterval used instead of setTimeout in save-then-navigate callbacks

  • Files:
  • pages/designer.vue:201-204 and :228-231
  • pages/postcreator.vue:148-151 and :177-180
  • pages/postdesigner.vue:182-185 and :196-199
  • What: Save/saveas success handlers use setInterval(function() { self.$router.go('/designer'); }, 5000) — the interval fires every 5 seconds indefinitely. The interval ID is discarded.
  • Why it matters: After a successful save, the page reloads every 5 seconds forever. Each reload creates a new component instance that registers a new interval, compounding. Within one session, dozens of concurrent intervals may fire.
  • Fix: Replace setInterval with setTimeout.

M3. isLoading={true} hardcoded in three template panels — spinner never clears

  • Files:
  • polotno-editor/templatesPanel.js:30
  • polotno-editor/carousaltemplatesPanel.js:30
  • polotno-editor/alltemplatesPanel.js:71
  • What: ImagesGrid is rendered with isLoading={true} as a constant, not tied to fetch state.
  • Why it matters: The loading spinner never clears even after data arrives. Staff see a perpetual overlay hiding the template thumbnails — the panels appear permanently broken. Compare to customUploads.js:193 in the same codebase, which correctly passes isLoading={loading}.
  • Fix: Add a loading state flag per panel, set to false after setPost(...), and pass isLoading={loading}.

M4. commonPagecount condition string built by interpolating user-selected IDs — SQL injection surface

  • Files:
  • pages/Template.vue:356condition:`isDeleted=0 and user_id=${this.selectdesigners.id} and approved=${this.selectedstatus.id}...`
  • pages/Template.vue:360, 375 — same pattern
  • What: The condition parameter is built by interpolating dropdown-selected .id values into a raw SQL fragment string that is sent to the BE's /commonPagecount endpoint.
  • Why it matters: If the BE uses the condition string directly in a SQL WHERE clause (consistent with the string-interpolation patterns noted in someli-doc/audit/designer-api/), a manipulated .id value delivers arbitrary SQL. Internal tool, but insider threat or compromised session is the threat model.
  • Fix: On the BE, parse and parameterise all filter inputs. On the FE, assert that selectdesigners.id and selectedstatus.id are integers (Number.isInteger) before dispatch.

Low / nits

L1. setInterval polling in AI-Design.vue has no clearInterval in beforeDestroy

  • File: pages/AI-Design.vue:101-103
  • What: this.timer = setInterval(() => { this.getReloadPosts() }, 60000) in mounted(). No beforeDestroy() in this component.
  • Fix: Add beforeDestroy() { clearInterval(this.timer); }.

L2. store/index.js uses namespaced: true but getters read state.auth from root

  • File: store/index.js:134-140
  • What: With namespaced: true, a getter's state argument is the module's own state, not the root. state.auth is therefore undefined. Currently harmless because the store is used as the root module (no parent module mounts it as a child), but the namespaced: true declaration is wrong and will silently break isAuthenticated / loggedInUser if the store is ever restructured.
  • Fix: Remove namespaced: true, or change the getters to accept and use rootState.

L3. Polotno-editor panels bypass the apptype auth header

  • Files: polotno-editor/brandkitPanel.js, customUploads.js, templatesPanel.js, carousaltemplatesPanel.js, alltemplatesPanel.js
  • What: These panels call axios.post(baseUrl + '/...') directly without setting the apptype header that middleware/axios.js adds to the Nuxt axios instance. The apptype header is the app-type discriminator used by designer-api.
  • Why it matters: If designer-api rejects or misbehaves on requests missing this header, all Polotno panel API calls silently fail or return wrong data.
  • Fix: Either proxy these calls through the Vue layer (Vuex action using this.app.$axios), or create an axios instance in the bundle that pre-sets the header.

Cross-cutting observations

  1. store.loadJSON called before createEditor in all editor pages. In designer.vue, templateEditor.vue, and carousaltemplateEditor.vue, the code fetches the existing design JSON and calls store.loadJSON(json) before calling createEditor({ container }). Because store is the module-level singleton, this technically works. However, createEditor may reset the store to defaults internally on some Polotno versions. The platform's index.js handles this by calling loadStateFromLocalStorage() inside createEditor after render, which is the correct pattern.

  2. this.$store.$axios used throughout Vue pages. Pages like Template.vue, Posts.vue, designer.vue call this.$store.$axios.post(...) instead of this.$axios. $store.$axios is an incidental injection from @nuxtjs/axios, not a documented API. This coupling is fragile.

  3. Pexels.js imports axios but never uses it (Pexels fetches use useInfiniteAPI/fetch). The import at line 4 is dead code on top of the duplicate at line 10.