Skip to content

Commit 95459d0

Browse files
committed
Constrain workspace photo DOM updates
Use the fixed authenticated workspace-photo route after uploads instead of reinterpreting an API-returned URL, and retain a focused source contract. Signed-off-by: Joseph Yaksich <gitcommit90@users.noreply.github.com>
1 parent 2cc5c04 commit 95459d0

3 files changed

Lines changed: 14 additions & 3 deletions

File tree

public/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@
3434
</head>
3535
<body class="h-screen w-screen overflow-hidden antialiased">
3636
<div id="app" class="h-full w-full"></div>
37-
<script type="module" src="/bundle.js?v=745230018af2"></script>
37+
<script type="module" src="/bundle.js?v=525ac42f9d00"></script>
3838
</body>
3939
</html>

src/client/settings.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ function adminPanel(): HTMLElement {
178178
const file = h("input", { type: "file", accept: "image/png,image/jpeg,image/webp,image/gif", class: "hidden" }) as HTMLInputElement;
179179
const presetColors = ["#c8552f", "#4f6d7a", "#8a6b7c", "#a67c52", "#7a6a4f", "#2e7d4f", "#2166b8", "#64748b"];
180180
const presetRow = h("div", { class: "mt-2 flex flex-wrap gap-2" });
181+
const applyWorkspacePhoto = (workspace: typeof S.workspace): void => {
182+
// The endpoint is fixed and same-origin; never turn an API-returned string
183+
// into an arbitrary DOM URL. This also keeps session tokens out of any
184+
// server-selected destination.
185+
S.workspace = workspace;
186+
const source = workspacePhotoSrc("/api/workspace/photo", Date.now());
187+
photo.src = source;
188+
document.querySelectorAll<HTMLImageElement>(".logo-asset").forEach((image) => { image.src = source; });
189+
};
181190
const applyPreset = async (hex: string): Promise<void> => {
182191
status.textContent = "Applying workspace color…";
183192
const canvas = document.createElement("canvas");
@@ -190,7 +199,7 @@ function adminPanel(): HTMLElement {
190199
const response = await fetch(apiUrl("/api/workspace/photo"), { method: "POST", headers: { authorization: `Bearer ${getToken()}`, "content-type": "image/png" }, body: blob });
191200
const result = await response.json().catch(() => ({}));
192201
if (!response.ok) { status.textContent = result.error || `HTTP ${response.status}`; return; }
193-
S.workspace = result.workspace; photo.src = workspacePhotoSrc(S.workspace.photo_url, Date.now()); document.querySelectorAll<HTMLImageElement>(".logo-asset").forEach((image) => { image.src = workspacePhotoSrc(S.workspace.photo_url, Date.now()); }); status.textContent = "Workspace photo updated.";
202+
applyWorkspacePhoto(result.workspace); status.textContent = "Workspace photo updated.";
194203
};
195204
for (const hex of presetColors) presetRow.append(h("button", { class: "h-8 w-8 rounded-lg border border-line shadow-sm transition hover:scale-105", style: `background:${hex}`, title: hex, onclick: () => { void applyPreset(hex); } }));
196205
file.onchange = async () => {
@@ -199,7 +208,7 @@ function adminPanel(): HTMLElement {
199208
const response = await fetch(apiUrl("/api/workspace/photo"), { method: "POST", headers: { authorization: `Bearer ${getToken()}`, "content-type": image.type }, body: image });
200209
const result = await response.json().catch(() => ({}));
201210
if (!response.ok) { status.textContent = result.error || `HTTP ${response.status}`; return; }
202-
S.workspace = result.workspace; photo.src = workspacePhotoSrc(S.workspace.photo_url, Date.now()); document.querySelectorAll<HTMLImageElement>(".logo-asset").forEach((image) => { image.src = workspacePhotoSrc(S.workspace.photo_url, Date.now()); }); status.textContent = "Workspace photo updated.";
211+
applyWorkspacePhoto(result.workspace); status.textContent = "Workspace photo updated.";
203212
};
204213
const save = async (): Promise<void> => {
205214
try { S.workspace = (await api<{ workspace: typeof S.workspace }>("/api/workspace", { method: "PATCH", body: { name: name.value.trim().slice(0, 100), theme: theme.value } })).workspace; applyWorkspaceTheme(); document.querySelectorAll<HTMLElement>("[data-workspace-name]").forEach((node) => { node.textContent = S.workspace.name; }); status.textContent = "Workspace settings saved."; }

test/workspace-interactions.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ test("profile, naming, routing, and usage language match the visible product con
4040
assert.match(app, /toBlob\(resolve, "image\/jpeg", 0\.86\)/, "the square avatar is compressed before upload");
4141
assert.match(app, /Photo ready\. Adjust the crop, then choose Save profile\./, "file choice does not instantly upload the raw photo");
4242
assert.match(settings, /maxlength: 100/, "workspace naming allows and caps a generous 100 characters");
43+
assert.match(settings, /workspacePhotoSrc\("\/api\/workspace\/photo", Date\.now\(\)\)/, "workspace photo updates use the fixed same-origin asset route instead of an API-returned DOM URL");
44+
assert.equal([...settings.matchAll(/workspacePhotoSrc\(S\.workspace\.photo_url, Date\.now\(\)\)/g)].length, 1, "only the already-loaded workspace value may initialize the preview; update responses never become image destinations");
4345
assert.match(app, /dataset: \{ workspaceName: "" \}/, "the complete workspace name has a stable wrapping hook");
4446
assert.match(settings, /Connection availability/, "connections use direct availability wording");
4547
assert.doesNotMatch(settings, /More connections/, "the ambiguous connections heading is gone");

0 commit comments

Comments
 (0)