One session at a time, please
I was recently working on a web app in which it was necessary to ensure that only one session at a time was active per user. The app in question is highly interactive and accumulates information from the user but does not commit it to the data store more often the once every few minutes (or on session termination) in order to limit disk write frequency.
If the user should log in a second time from another browser window or computer without first logging out their old session, I needed to move the accumulated information to the new session and invalidate the old session.
This proved to be a little tricky and I gained some insight into Seaside session handling in the process.
Maybe this shouldn't really be called session stealing, but rather session data transfer or something, but stealing sessions sounds like so much more fun.
Some background
The application in question uses TFLogin for managing logins and for saving per-user data. However, the general approach should be usable with other user management schemes.
Two things:
#logoutOldSessions
This method is called when the user logs in to logout any old session of the user and steal its user data objects.
We look through all the sessions for our application. Ignoring sessions with no user (these are sessions not yet logged in), other user's sessions, and our own session, we are left with - at most one - session which is a previous session of the current user. (At most one, because this method is executed on every login and logs out any previous session that it finds.)
We send #logout: to the previous session's presenter instance, passing the session to be logged out. We pass the old session because self>>#session will always return the current session, no matter which application instance's code is being executed.
(#logout is described in more detail in the next section.)
Here is the code for #logoutOldSessions that is called from within the TLLoginComponent>>#onAnswer: block:
logoutOldSessions
"Find any old session that hasn't saved its user data, retrieve
the user data from it and log it out. There should be at
most one such session."
| otherSessionUserData |
self session application sessions do: [ :each |
each user isNil ifFalse: [
each user userId = self session user userId ifTrue: [
each == self session ifFalse: [
otherSessionUserData := each presenter logout: each ]]]].
otherSessionUserData isNil
ifFalse: [
self session user applicationProperties
at: 'userdata'
put: otherSessionUserData].
#logout:
This method is meant to be invoked from a different session than the one in which the app was instantiated. The session is passed as the argument to the method.
First we save our possibly uncommitted user data in a temporary variable. The we send #logout: to our TLLoginComponent instance, passing the session we were provided. (TLLogincomponent>>#logout: is a special alternative to the plain TLLogincomponent>>#logout method. It is provided precisely to allow logout of one session by another session. This special method does not save the user object but simply sets it to nil.)
We then abort the background process that is in charge of periodically saving modified user data and unregister the session.
Finally, we return the userdata to the calling #logoutOldSessions method, where it will be used to initialize the new session.
logout: session
"This is invoked by other sessions."
| userdata |
userdata := session user
applicationproperties at: 'userdata'.
loginComponent logout: session.
session abortBackgroundProcesses.
session unregister.
^ userdata
Conclusion
This has proven to be reliable and avoids using the disk to transfer data between sessions unnecessarily. There remains what to do when the user returns to the old session, which still appears to be logged in from the browser's viewpoint, and tries to continue work there. There is a solution to this, but that is a subject for the next article...