mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
Three reports, one story: the application kept treating a dead session as an ordinary navigation. **Landing on your own profile picture after signing in.** Symfony saves the login target in ExceptionListener::setTargetPath(), whose test is `isMethodSafe() && !isXmlHttpRequest()` — and an `<img src="/…/avatar">` is both. So the last unauthenticated thing the browser happened to fetch became the place the next login went. The session ends, the page is still on screen, the browser re-requests the avatar, that 302 saves an image URL, and login redirects to it. Whichever subresource lost the race decided the destination, which is why it looked intermittent rather than broken. The entry point is where to undo it, because it runs after the save. Anything that is not a document navigation has the saved path removed again. The test is two-sided rather than a list of extensions: `Sec-Fetch-Dest: document` is what a browser says about a real navigation, and an Accept mentioning text/html covers a Turbo visit — which is a fetch, and reports `Sec-Fetch-Dest: empty`. Both halves are pinned, so the fix cannot degrade into "never remember anything": asking for /settings while signed out still returns you to /settings. **Logout went through Turbo.** The shell survived it — controllers still mounted and polling with a cookie that no longer existed, the Mercure subscription still open, and Turbo's page cache able to put a signed-in snapshot back with the back button. data-turbo="false". **A session ending underneath an open page** made Turbo swap the login form into the application shell: a password box wearing the sidebar, with every controller on the page still running and failing. There is a bar now. The interesting decision is in that bar. "Reload the page" is the obvious response and it is wrong exactly when it matters: a half-written mail lives in a contenteditable, and the autosave that would preserve it is a POST — the thing that has just stopped working. So with nothing being written it offers a straightforward sign-in in this tab, and with something being written it offers a new tab instead and leaves this one untouched, text and all. The cookie is shared across tabs, so coming back here just works and the next autosave lands. Being wrong in that direction costs a tab; being wrong the other way costs what somebody wrote. Also: the compose window's close button gets an aria-label (CODE-REVIEW U-01). Its only content is an icon, so with `title` alone the accessible name depended on a screen reader choosing to fall back to it, and on touch it was unreachable — while above md it is the only way out of the window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
47 lines
2.1 KiB
Twig
47 lines
2.1 KiB
Twig
{#
|
|
The bar that appears when the session ends underneath a page that is still open.
|
|
|
|
Rendered on every page and hidden, rather than built in JavaScript, because
|
|
every string in it needs translating and translation is a server-side concern
|
|
here. The controller only unhides it and chooses which of the two sentences to
|
|
show.
|
|
|
|
Two sentences, and the difference is the whole point of the feature. With
|
|
nothing being written, ending up on the login page is what the user wants and
|
|
the bar just says so. With a half-written mail on screen, a redirect would throw
|
|
away everything since the last autosave — so the offer is to sign in in a new
|
|
tab and come back to a page that still has the text in it.
|
|
#}
|
|
<div id="session-expired"
|
|
data-controller="core--session-guard"
|
|
data-core--session-guard-login-url-value="{{ path('app_login') }}"
|
|
class="hidden fixed inset-x-0 top-0 z-[300] px-4 py-3
|
|
bg-surface border-b-2 border-danger shadow-float"
|
|
role="alert"
|
|
aria-live="assertive">
|
|
<div class="mx-auto flex max-w-3xl items-center gap-3 text-sm">
|
|
<i class="fa-solid fa-circle-exclamation shrink-0 text-danger" aria-hidden="true"></i>
|
|
|
|
<div class="min-w-0 flex-1">
|
|
<p class="font-medium text-ink">{{ 'session.expired.heading'|trans }}</p>
|
|
<p class="text-ink-soft"
|
|
data-core--session-guard-target="body"
|
|
data-body="{{ 'session.expired.body'|trans }}"
|
|
data-body-writing="{{ 'session.expired.body_writing'|trans }}"></p>
|
|
</div>
|
|
|
|
<a href="{{ path('app_login') }}"
|
|
data-turbo="false"
|
|
data-core--session-guard-target="signIn"
|
|
data-label="{{ 'session.expired.sign_in'|trans }}"
|
|
data-label-new-tab="{{ 'session.expired.sign_in_new_tab'|trans }}"
|
|
class="shrink-0 rounded-full bg-accent px-4 py-2 font-medium text-on-accent"></a>
|
|
|
|
<button type="button"
|
|
data-action="click->core--session-guard#reload"
|
|
class="shrink-0 rounded-full border border-line px-4 py-2 font-medium text-ink-soft hover:bg-hover">
|
|
{{ 'session.expired.reload'|trans }}
|
|
</button>
|
|
</div>
|
|
</div>
|