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 insomeli-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_KEYand inject via the.envfile (Parcel inlinesprocess.envat build time). Ensure.envis in.gitignore.
C2. Duplicate import axios in Pexels.js — Pexels panel fails to build¶
- File:
polotno-editor/Pexels.js:4and: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
axiosis not actually used inPexels.js(Pexels API calls go throughuseInfiniteAPI/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:36—v-html="resContent"whereresContent = 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:38—v-html="rs.content"wherers.content = element.replace(/\n/g, '<br>') + '.'pages/pdfToContent.vue:36—v-html="p.content"from a PDF extraction endpoint- What: API response text from LLM and PDF endpoints is piped directly through
v-htmlwith 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-htmlwith{{ resContent }}and CSSwhite-space: pre-wrapto preserve line breaks. If rich formatting is required, sanitize with DOMPurify before binding tov-html.
H2. updatefeed mutation bypasses Vue 2 reactivity — approval badge never updates¶
- File:
store/index.js:125-128 - What:
state.alluserdesignlist[index].approved = item.apdirectly mutates a property on an existing array element by index. Vue 2's reactivity system cannot detect this change; it requiresVue.setor array replacement. - Why it matters: After a staff member approves or rejects a template in
Template.vue(which commitsupdatefeedon lines 264, 306, 329), the card background colour (driven byitem.approved) does not change. The UI appears to silently ignore the action until a page reload. - Fix:
H3. Designer editor has no autosave — work lost on crash (fork-specific bug vs platform)¶
- Files:
polotno-editor/index.js(entire file) vssomeli-platform/polotno-editor/index.js:17-41 - What: The platform fork adds
store.on("change", saveStateToLocalStorage)at module load, which debounces design state tolocalStorageevery 2 s. The designer fork creates the store and callscreateEditorwithout any change subscription. There is noloadStateFromLocalStoragecall 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-41verbatim into the designer'sindex.js. Three additions:const href,saveStateToLocalStorage(debounced 2 s),store.on("change", saveStateToLocalStorage). Also callloadStateFromLocalStorage()insidecreateEditorbeforeroot.render.
H4. handleScroll defined outside methods: — scroll listener registers undefined, never removed¶
- Files:
pages/dashboard.vue:82-103pages/Template.vue:415-433pages/Posts.vue:663-683- What: In all three pages,
handleScroll()andcreated()are declared as top-level keys of the component options object, sibling tomethods:,mounted:, etc. — not insidemethods:. Vue 2 ignores unknown option keys, sothis.handleScrollisundefined.created()callswindow.addEventListener("scroll", this.handleScroll), which registersundefined. NobeforeDestroyhook 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
handleScrollinside themethods:object. AddbeforeDestroy() { window.removeEventListener("scroll", this.handleScroll); }.
Medium¶
M1. get_editId_from_local_storage event listener registered without cleanup¶
- Files:
pages/designer.vue:66pages/templateEditor.vue:143pages/templateEditor1.vue:150pages/carousaltemplateEditor.vue:183- What: Each editor page calls
window.addEventListener("get_editId_from_local_storage", (event) => { ... })inmounted()with an anonymous arrow function. None have abeforeDestroythat removes it. - Why it matters: If the tab stays open and the component re-mounts, each mount adds another listener. When
templatesPanel.js:81dispatches the event, all accumulated listeners fire — settingtempIdoreditidmultiple times and triggering duplicategetEditDesignList+getcolorEventAPI calls. - Fix: Store the handler reference on the instance and remove it in
beforeDestroy:
M2. setInterval used instead of setTimeout in save-then-navigate callbacks¶
- Files:
pages/designer.vue:201-204and:228-231pages/postcreator.vue:148-151and:177-180pages/postdesigner.vue:182-185and: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
setIntervalwithsetTimeout.
M3. isLoading={true} hardcoded in three template panels — spinner never clears¶
- Files:
polotno-editor/templatesPanel.js:30polotno-editor/carousaltemplatesPanel.js:30polotno-editor/alltemplatesPanel.js:71- What:
ImagesGridis rendered withisLoading={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:193in the same codebase, which correctly passesisLoading={loading}. - Fix: Add a
loadingstate flag per panel, set tofalseaftersetPost(...), and passisLoading={loading}.
M4. commonPagecount condition string built by interpolating user-selected IDs — SQL injection surface¶
- Files:
pages/Template.vue:356—condition:`isDeleted=0 and user_id=${this.selectdesigners.id} and approved=${this.selectedstatus.id}...`pages/Template.vue:360, 375— same pattern- What: The
conditionparameter is built by interpolating dropdown-selected.idvalues into a raw SQL fragment string that is sent to the BE's/commonPagecountendpoint. - Why it matters: If the BE uses the condition string directly in a SQL
WHEREclause (consistent with the string-interpolation patterns noted insomeli-doc/audit/designer-api/), a manipulated.idvalue 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.idandselectedstatus.idare 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)inmounted(). NobeforeDestroy()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'sstateargument is the module's own state, not the root.state.authis thereforeundefined. Currently harmless because the store is used as the root module (no parent module mounts it as a child), but thenamespaced: truedeclaration is wrong and will silently breakisAuthenticated/loggedInUserif the store is ever restructured. - Fix: Remove
namespaced: true, or change the getters to accept and userootState.
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 theapptypeheader thatmiddleware/axios.jsadds to the Nuxt axios instance. Theapptypeheader is the app-type discriminator used bydesigner-api. - Why it matters: If
designer-apirejects 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¶
-
store.loadJSONcalled beforecreateEditorin all editor pages. Indesigner.vue,templateEditor.vue, andcarousaltemplateEditor.vue, the code fetches the existing design JSON and callsstore.loadJSON(json)before callingcreateEditor({ container }). Becausestoreis the module-level singleton, this technically works. However,createEditormay reset the store to defaults internally on some Polotno versions. The platform'sindex.jshandles this by callingloadStateFromLocalStorage()insidecreateEditorafter render, which is the correct pattern. -
this.$store.$axiosused throughout Vue pages. Pages likeTemplate.vue,Posts.vue,designer.vuecallthis.$store.$axios.post(...)instead ofthis.$axios.$store.$axiosis an incidental injection from@nuxtjs/axios, not a documented API. This coupling is fragile. -
Pexels.jsimportsaxiosbut never uses it (Pexels fetches useuseInfiniteAPI/fetch). The import at line 4 is dead code on top of the duplicate at line 10.