Here's what I ended up building.
To answer my own questions:
Should I be using some sort of application controller filter (before or around) to check to see if the app is locked? I am using a before_filter in my Application Controller to check whether the UI is locked. If it's locked, I redirect to the "locked" screen. I then added a skip_before_filter to the relevant actions in my Sessions Controller (essentially ignoring that before_filter when the user is navigating to the "locked" screen and related lock/unlock actions in the Sessions controller.
Or is this something that should be handled in the view layer? In the view layer, I mostly just needed to create the actual "locked" screen (where a user enters credentials to unlock the app), and I make sure to hide navigation elements while the UI is locked (so the user isn't confused as to why they click "Edit Profile" and don't leave the lock screen).
I'm using cookies to record the current state of the UI (locked or not). Here are some code snippets:
application_controller.rb
before_filter :confirm_unlocked_ui
private
def confirm_unlocked_ui
if signed_in? && locked_ui?
redirect_to locked_path
end
end
sessions_helper.rb
def locked_ui?
session[:locked] == '1'
end
def lock_ui
session[:locked] = '1'
session[:locked_by] = current_user.id
session[:locked_at] = Time.zone.now
end
def unlock_ui
session.delete(:locked)
session.delete(:locked_by)
session.delete(:locked_at)
end
sessions_controller.rb
def lock
session[:return_to] = params[:return_to] if params[:return_to]
lock_ui
redirect_to locked_path
end
def locked
#essentially just a view
end
def unlock
user = User.find_by_id(params[:session][:unlock_user_id])
if user && user.authenticate_with_pin(params[:session][:pin])
cookies[:auth_token] = user.auth_token
unlock_ui
redirect_back_or root_path
else
flash.now[:error] = "Invalid PIN."
render 'locked'
end
end
def destroy
sign_out
unlock_ui
redirect_to root_path
end
I really don't know how "good" this solution is, and one reason I'm posting it here is to see if others come up with better ideas or see any issues lurking with how I've done this.
Some more subtle things you might want to think about if you're trying this:
- I'm using CanCan for authorization, and I've left that out here. If you have role-based authorization or something like that, be sure to check that this works for each role.
- Notice that I allow users to 'sign out' if they don't want to 'unlock'. I do this using the 'destroy' action my sessions controller and I make sure to 'unlock_ui' before I redirect them after signing out. If I didn't do this, then the app would still be "locked" the next time they log in from that browser.
- locked_path is an alias for Sessions#locked (basically just a view/form) that responds to a get request.
- Sessions#Lock also responds to get requests.
- Sessions#Unlock is what is called when the form is submitted from the "locked" view.