Imagine an app with posts. A post has comments. The comments form is inside a Turbo Frame. Anyone can view a post and its comments. Users must be signed in to add comments. Devise is used for authentication. If a user is signed in when they click a link to add a new comment then the Turbo Frame should be replaced with the new comment form. If a user is not signed in when they click the link to add a new comment then the user should be redirected to the new session page. It seems that this requires the server to have the ability to break out of the Turbo Frame if there is a 401 redirect. Is this possible? Is there another way this can be achieved?
maybe when rendering the link to add a new comment it checks if the user is logged and then renders a link to the login page if not logged in, or a redirect to add a new comment.
Yeah if you’re able to change the link if someone’s not logged in, it’s fairly easy. You’d just add data-turbo-frame="_top"
attribute to the link — and that’s it.
Changing the link is not an ideal solution because it doesn’t prevent this sequence of events:
- The user logs in.
- The form is rendered for the logged in user.
- The user’s session expires or they delete their session cookie.
- The user submits the form rendered in step 2. The server redirects to the new session page, and the Turbo Frame is replaced with empty content.
Same problem here. My try was
document.addEventListener("turbo:before-visit", (event) => {
// check if 401 redirection here
})
Unfortunately the event was not triggered.
After 2 days of trial and error, here is a workaround. Hopefully there will be an official solution to request interception soon.
# In my layout, I attached a turbo controller to the `body` element.
# <b>turbo:before-fetch-response</b> is one of the two events I found working in turbo frames.
# The other one is <b>turbo:before-fetch-request</b>
body data-controller="unauthorized" data-action="turbo:before-fetch-response->unauthorized#intercept"
= yield
// in unauthorized_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
intercept(event) {
const {url} = event.detail.fetchResponse.response;
// I was not able to intercept the 401 response, only the one after which.
// This is response is a 200 response. I had to use the url to match.
// This method is sufficient for my project, but not enough to be an general solution
if (url.match("runtime/project_users/projects/\\d+/sign_in")) {
event.preventDefault()
// Here you may ask, "shouldn't we redirect to the url in the url variable above?"
// The reason being, devise stores the location before signing out by default,
// to redirect the user back after a successful authorization. The url before signing out
// here, is the turbo frame request, which is part of the the current page, therefore may not
// an ideal target for the user to visit. The current page, which is the container of the turbo
// frame, is a more appropriate candidate.
window.location = window.location.href
}
}
}
I was wondering, what IS the expected behavior for an action that is inside of a turbo frame, but behind some sort of authentication check.
Say you have a public “+1” button on a publicly available page, but when you click the like button, your controller action checks for an authenticated user, and you want to pop out with a flash that says “in order to +1 this, you need to log in first”.
Decorating the frame with special markup if the user is logged out seems kinda gnarly, and could be brittle if there are multiple sessions or previous page loads hanging around.
If this topic wasn’t about devise directly, what would an authentication library do to make Turbo follow this pattern?