feat(tables): gate +Add row on path verbs.c + cap-toast on 403
Two server-aligned signals on save paths:
- +Add row button: fetches /.profile/access?path=<current dir> via
zddc.cap.at() once on load; if path_verbs doesn't include 'c'
the button disables with a tooltip ("You don't have create
access in this folder."). Async race-window is the same as any
other path-scoped fetch — server still gates the POST so a
stale client gets a 403 toast on click rather than a silent
accept.
- 403 on save/create: previously fell into the generic
"http-error" bucket with a console warn; now branches into
zddc.cap.handleForbidden which renders an error toast naming the
missing verb. When the path-scoped view reports an elevation
grant covering that verb, the toast appends an Elevate button.
Per-row writability stays computed server-side for now — tables
walks rows via FS-API-style handles that don't surface the listing
verbs string. A follow-on pass can switch the row walk to raw
listing entries and gate row.editable on each entry's verbs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fbfb8d15a1
commit
34208a5bd7
2 changed files with 49 additions and 0 deletions
|
|
@ -125,6 +125,33 @@
|
||||||
addRowBtn.addEventListener('keydown', function (ev) {
|
addRowBtn.addEventListener('keydown', function (ev) {
|
||||||
if (ev.key === 'Enter' || ev.key === ' ') handleAdd(ev);
|
if (ev.key === 'Enter' || ev.key === ' ') handleAdd(ev);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Permission gate: fetch the path-scoped verbs for the
|
||||||
|
// current directory and disable + Add row when the
|
||||||
|
// cascade denies create. Async — the button shows up
|
||||||
|
// optimistically and disables a tick later if the
|
||||||
|
// server says no, which is the same race window every
|
||||||
|
// path-scoped fetch has. Server still gates the POST,
|
||||||
|
// so the worst case is a 403 toast on click.
|
||||||
|
if (window.zddc && window.zddc.cap) {
|
||||||
|
window.zddc.cap.at(location.pathname).then(function (view) {
|
||||||
|
if (!view) return;
|
||||||
|
var verbs = view.path_verbs || '';
|
||||||
|
if (verbs.indexOf('c') === -1) {
|
||||||
|
addRowBtn.classList.add('is-disabled');
|
||||||
|
addRowBtn.setAttribute('aria-disabled', 'true');
|
||||||
|
addRowBtn.title = "You don't have create access in this folder.";
|
||||||
|
// Swallow clicks so the no-op feedback is the
|
||||||
|
// tooltip, not a 403 toast on submission.
|
||||||
|
addRowBtn.addEventListener('click', function (ev) {
|
||||||
|
if (addRowBtn.classList.contains('is-disabled')) {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -316,6 +316,17 @@
|
||||||
return { status: 'invalid', errors: errs };
|
return { status: 'invalid', errors: errs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.status === 403) {
|
||||||
|
setRowState(rowId, 'errored');
|
||||||
|
if (window.zddc && window.zddc.cap) {
|
||||||
|
window.zddc.cap.handleForbidden(resp, {
|
||||||
|
context: 'Save row',
|
||||||
|
path: location.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { status: 'forbidden' };
|
||||||
|
}
|
||||||
|
|
||||||
// Other status — generic error.
|
// Other status — generic error.
|
||||||
console.warn('[tables] save returned', resp.status);
|
console.warn('[tables] save returned', resp.status);
|
||||||
setRowState(rowId, 'errored');
|
setRowState(rowId, 'errored');
|
||||||
|
|
@ -395,6 +406,17 @@
|
||||||
return { status: 'invalid', errors: errs };
|
return { status: 'invalid', errors: errs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.status === 403) {
|
||||||
|
setRowState(rowId, 'errored');
|
||||||
|
if (window.zddc && window.zddc.cap) {
|
||||||
|
window.zddc.cap.handleForbidden(resp, {
|
||||||
|
context: 'Add row',
|
||||||
|
path: location.pathname
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { status: 'forbidden' };
|
||||||
|
}
|
||||||
|
|
||||||
console.warn('[tables] createRow returned', resp.status);
|
console.warn('[tables] createRow returned', resp.status);
|
||||||
setRowState(rowId, 'errored');
|
setRowState(rowId, 'errored');
|
||||||
return { status: 'http-error', code: resp.status };
|
return { status: 'http-error', code: resp.status };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue