mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Stop eight dashboard views reporting success for requests the server refused
Each of these views carries its own copy of a fetch helper ending in `.then(r => r.json())`. A 403, 404 or 500 body resolves as an ordinary value, so the surrounding try/catch is unreachable and every handler treats the failure as success. The shared client in api.js has always thrown on !res.ok; these local copies never did. Two concrete consequences, both of which tell the operator something untrue: - The layout editor renders a Delete button on built-in templates for everyone. The server returns 403. The handler shows "Layout deleted" and re-renders the list with the template still sitting there. - A rejected platform-role change in Admin shows "Role updated", and the revert that would put the dropdown back lives only in the dead catch — so the UI keeps displaying a value the server refused. The same control in Settings uses the throwing client, so the two pages disagree about whether the change happened. All eight now match the shared contract: reject on !ok with the server's own message, and treat 401 as session expiry the way api.js does. This makes previously-silent failures visible, which is the point — some of them will surface refusals that were always happening. The layout template Delete button, for instance, is now honestly reported as refused rather than falsely confirmed; whether that button should be shown at all is a separate question. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
This commit is contained in:
parent
d978a5d2a6
commit
9ea1b5e07b
|
|
@ -2,7 +2,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { esc } from '../utils.js';
|
||||
import { t } from '../i18n.js';
|
||||
|
||||
const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}` }}).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
export async function render(container) {
|
||||
container.innerHTML = `
|
||||
|
|
|
|||
|
|
@ -12,7 +12,20 @@ import { openTypeToConfirmModal } from '../components/type-to-confirm-modal.js';
|
|||
import { mapMutationError } from './workspace-members.js';
|
||||
|
||||
const headers = () => ({ Authorization: `Bearer ${localStorage.getItem('token')}`, 'Content-Type': 'application/json' });
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: headers(), ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: headers(), ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
// #14: the platform user-management dropdown manages users.role (the
|
||||
// PLATFORM-level role) only - workspace/org roles are managed in the members
|
||||
|
|
|
|||
|
|
@ -2,7 +2,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { t } from '../i18n.js';
|
||||
import { esc } from '../utils.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
export async function render(container) {
|
||||
const hash = window.location.hash;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { t, tn } from '../i18n.js';
|
||||
import { esc } from '../utils.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
export async function render(container) {
|
||||
const hash = window.location.hash;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { esc } from '../utils.js';
|
||||
import { t } from '../i18n.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
export async function render(container) {
|
||||
const devices = await api.getDevices();
|
||||
|
|
|
|||
|
|
@ -8,7 +8,20 @@ import {
|
|||
dragArmMode, LONG_PRESS_MS, DEFAULT_NEW_MIN,
|
||||
} from '../lib/schedule-grid.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
// Teardown registered during render (resize listener, etc). Declared here rather than beside
|
||||
// cleanup() so it is initialised before any render can push to it.
|
||||
|
|
|
|||
|
|
@ -3,7 +3,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { t, tn } from '../i18n.js';
|
||||
import { esc } from '../utils.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
export async function render(container) {
|
||||
const hash = window.location.hash;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,20 @@ import { showToast } from '../components/toast.js';
|
|||
import { t } from '../i18n.js';
|
||||
import { hydrateAuthImages } from '../utils.js';
|
||||
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(r => r.json());
|
||||
// A refused request must reject, not resolve.
|
||||
//
|
||||
// This helper used to end in `.then(r => r.json())`, so a 403/404/500 body resolved as an ordinary
|
||||
// value and the surrounding try/catch was unreachable — every handler took the failure for success.
|
||||
// Concretely: deleting a built-in layout template showed "Layout deleted" while the server had
|
||||
// returned 403 and the template was still there, and a rejected platform-role change showed "Role
|
||||
// updated" while the dropdown kept displaying a value the server refused (its revert lives only in
|
||||
// the dead catch). The shared client in api.js has always thrown on !res.ok; these local copies did
|
||||
// not. Same contract now, including the 401 session-expiry reload.
|
||||
const API = (url, opts = {}) => fetch('/api' + url, { headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('token')}`, ...opts.headers }, ...opts }).then(async (r) => {
|
||||
if (r.status === 401) { localStorage.removeItem('token'); window.location.reload(); throw new Error('Session expired'); }
|
||||
if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || `Request failed (${r.status})`); }
|
||||
return r.json();
|
||||
});
|
||||
|
||||
// Widget type ids only — name + desc are looked up via t() so they switch
|
||||
// language with the rest of the UI.
|
||||
|
|
|
|||
Loading…
Reference in a new issue