fix: the signed-out preview is reachable from the page, from any path
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m51s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 49s
Forgejo Docker Build / Root app tests (push) Successful in 49s
Forgejo Android APK / Build signed APK (push) Successful in 1m51s
Forgejo Docker Build / Build Docker image (push) Successful in 18s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
The server side has worked since this morning, but no browser could reach it. authFetch rejects every /api request that has no account before it is sent, and the four preview endpoints were not on its short list — so the status call that decides whether to show the login screen never left the browser, and the screen was always shown. The list now mirrors the server's own allow-list exactly: status, examples, chat, chat/stream, and nothing else. Preview now begins from any path. A visitor landing on the root met the login wall while /assistant did not, which read as "preview doesn't work"; both now enter the assistant, and the URL follows. Reaching for anything that needs an account raises the sign-in screen through one hook in authFetch rather than a check on every control — but only for something the visitor did. The page also fetches saved chats and config in the background on load, and the first version raised the screen for those too, burying the assistant before a word was typed. The hook is gated on navigator.userActivation. The HIPAA notice is hidden on that screen in preview: it is an invitation to sign in, not the compliance notice a clinician sees on first login. Verified in a browser: landing on / and on /assistant both show the assistant with no login wall and no HIPAA text; clicking Workspace raises sign-in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
This commit is contained in:
parent
97454a8941
commit
004fb80a60
4 changed files with 62 additions and 12 deletions
|
|
@ -1414,6 +1414,9 @@ body.assistant-preview .assistant-preview-note { display:flex; }
|
|||
/* Collapsed menus hide their contents; the card goes with them. */
|
||||
/* A preview visitor has no account to show. */
|
||||
body.assistant-preview .account-card { display:none; }
|
||||
/* The sign-in prompt a preview visitor meets is an invitation, not the
|
||||
compliance notice a clinician sees on first login. */
|
||||
body.assistant-preview .hipaa-notice { display:none; }
|
||||
|
||||
/* ── Search palette ──────────────────────────────────────────────────────────
|
||||
One component in both views; the view decides what it searches. */
|
||||
|
|
|
|||
|
|
@ -180,19 +180,23 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// assistant instead of meeting a wall. Everything else still needs an
|
||||
// account, and the server enforces that independently — this only decides
|
||||
// whether to show the login screen.
|
||||
if (window.location && window.location.pathname === '/assistant') {
|
||||
fetch('/api/clinical-assistant/status', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.preview) return enterPreview();
|
||||
authScreen.style.display = 'flex';
|
||||
})
|
||||
.catch(function() { authScreen.style.display = 'flex'; });
|
||||
return;
|
||||
}
|
||||
authScreen.style.display = 'flex';
|
||||
// Any path, not only /assistant: with preview on, a visitor landing on the
|
||||
// root should meet the assistant, not a wall. enterPreview moves them there.
|
||||
fetch('/api/clinical-assistant/status', { credentials: 'same-origin' })
|
||||
.then(function(r) { return r.ok ? r.json() : null; })
|
||||
.then(function(data) {
|
||||
if (data && data.success && data.preview) return enterPreview();
|
||||
authScreen.style.display = 'flex';
|
||||
})
|
||||
.catch(function() { authScreen.style.display = 'flex'; });
|
||||
}
|
||||
|
||||
// Raised by authFetch when a preview visitor reaches for something that needs
|
||||
// an account. The screen stays an overlay, so their chat is still underneath.
|
||||
document.addEventListener('preview-needs-account', function() {
|
||||
if (authScreen && document.body.classList.contains('assistant-preview')) authScreen.style.display = 'flex';
|
||||
});
|
||||
|
||||
// The auth screen is an overlay over an already-rendered app, so entering
|
||||
// preview is simply declining to raise it.
|
||||
function enterPreview() {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,15 @@ if (!window.__fetchAuthIntercepted) {
|
|||
'/api/auth/verify-email', '/api/auth/resend-verification',
|
||||
'/api/auth/oidc', '/api/auth/oidc-status', '/api/auth/registration-status'
|
||||
]);
|
||||
// The signed-out preview. Same four paths the server allow-lists; nothing
|
||||
// else may leave the browser without an account. Until these were here the
|
||||
// status call was rejected before it was ever sent, so the server-side
|
||||
// preview could never be reached from the page.
|
||||
var previewPaths = new Set([
|
||||
'/api/clinical-assistant/status', '/api/clinical-assistant/examples',
|
||||
'/api/clinical-assistant/chat', '/api/clinical-assistant/chat/stream'
|
||||
]);
|
||||
function inPreview() { return document.body && document.body.classList.contains('assistant-preview'); }
|
||||
|
||||
// Narrow transition requests bypass the frozen clinical transport. Logout
|
||||
// headers are captured before freezing; its verification is cookie-only.
|
||||
|
|
@ -40,6 +49,14 @@ if (!window.__fetchAuthIntercepted) {
|
|||
if (boundary.blocked()) return Promise.reject(boundary.error());
|
||||
// No clinical module may preload a previous cookie's data on the login screen.
|
||||
if (!ticket && !authPaths.has(url.pathname) && url.pathname !== '/api/auth/me' && url.pathname !== '/api/models') {
|
||||
if (previewPaths.has(url.pathname)) return rawFetch(input, Object.assign({}, init, { credentials: 'same-origin' }));
|
||||
// A preview visitor reaching for anything else is exactly the moment to
|
||||
// ask them to sign in — one hook here, not a check on every button. Only
|
||||
// for something the visitor did, though: the page also fetches saved
|
||||
// chats and config in the background on load, and raising the sign-in
|
||||
// screen for those buried the assistant under it before a word was typed.
|
||||
var gesture = navigator.userActivation ? navigator.userActivation.isActive : false;
|
||||
if (inPreview() && gesture) document.dispatchEvent(new CustomEvent('preview-needs-account', { detail: { path: url.pathname } }));
|
||||
return Promise.reject(boundary.error());
|
||||
}
|
||||
var login = (url.pathname === '/api/auth/login' || url.pathname === '/api/auth/register')
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ test('a preview visitor has no identity, so nothing can be owned or billed', ()
|
|||
test('preview is entered by declining the login overlay, not by faking a session', () => {
|
||||
const auth = read('public/js/auth.js');
|
||||
const fn = auth.slice(auth.indexOf('function showAuthScreen()'), auth.indexOf('// ── Check for SSO redirect'));
|
||||
assert.match(fn, /pathname === '\/assistant'/, 'only the assistant page previews');
|
||||
// Any path previews, not only /assistant: a visitor landing on the root should
|
||||
// meet the assistant, not a wall. enterPreview moves them onto that tab.
|
||||
assert.doesNotMatch(fn, /pathname === '\/assistant'/, 'the root previews too');
|
||||
assert.match(fn, /data\.success && data\.preview/, 'and only when the server says so');
|
||||
assert.match(fn, /authScreen\.style\.display = 'flex';/, 'anything else raises the login screen');
|
||||
// No token, no user object, no stored credential — the overlay is simply not
|
||||
|
|
@ -90,4 +92,28 @@ test('reaching for the workspace is where a preview visitor is asked to sign in'
|
|||
assert.ok(css.includes('body.assistant-preview ' + hidden), hidden + ' is hidden in preview');
|
||||
}
|
||||
assert.match(read('public/components/assistant.html'), /assistant-preview-note/, 'and the state is stated plainly');
|
||||
// The prompt a preview visitor meets is an invitation, not the compliance notice.
|
||||
assert.ok(css.includes('body.assistant-preview .hipaa-notice { display:none; }'), 'no HIPAA notice in preview');
|
||||
});
|
||||
|
||||
test('the browser lets exactly the preview endpoints out without an account, and nothing else', () => {
|
||||
// Every /api fetch without an account is rejected before it is sent. Until the
|
||||
// preview paths were allowed here, the status call never reached the server,
|
||||
// so the server-side preview was unreachable from the page.
|
||||
const af = read('public/js/authFetch.js');
|
||||
const list = af.slice(af.indexOf('var previewPaths'), af.indexOf('function inPreview'));
|
||||
for (const p of ['/api/clinical-assistant/status', '/api/clinical-assistant/examples',
|
||||
'/api/clinical-assistant/chat', '/api/clinical-assistant/chat/stream']) {
|
||||
assert.ok(list.includes("'" + p + "'"), p + ' is allowed');
|
||||
}
|
||||
assert.doesNotMatch(list, /saved-chats|\/config|\/images|\/admin/, 'nothing that needs an account');
|
||||
assert.match(af, /if \(previewPaths\.has\(url\.pathname\)\) return rawFetch/, 'they go out with no ticket');
|
||||
// Anything else a preview visitor reaches for raises sign-in via one hook,
|
||||
// rather than a check on every button.
|
||||
// ...and only for something the visitor did. The page fetches saved chats and
|
||||
// config in the background on load; raising sign-in for those buried the
|
||||
// assistant before a word was typed.
|
||||
assert.match(af, /navigator\.userActivation \? navigator\.userActivation\.isActive : false/);
|
||||
assert.match(af, /if \(inPreview\(\) && gesture\) document\.dispatchEvent\(new CustomEvent\('preview-needs-account'/);
|
||||
assert.match(read('public/js/auth.js'), /addEventListener\('preview-needs-account'/, 'and auth.js raises the screen');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue