Adding login functionality to Netzke
Something I've been working on for a while was making a complete, uniform ExtJS business administration system based on Ruby on Rails 3 using Netzke. Netzke is a great library that brings DRY power to ExtJS and Rails, although it's still in the works and functionality is added to is on a very regular basis.
Something that I wasn't able to do is integrate authentication in the application. The NetzkeController that is used to process every server call in Netzke, always extended the ApplicationController (something which in my application wouldn't work, because I use an AdminController) and there was no real way to manipulate it, because the routing was tied to that NetzkeController.
So I worked out my specific use case and contacted the author of Netzke, Max Gorin, through the Netzke Google Group. He then worked out a way to be able to create your own Netzke controller, including all Netzke controller functionality, so that I would be able to catch authentication requests and process them differently. Also, this way I was able to skip authentication for certain actions. Let me show you what I did.
The routes
devise_for :admin_users netzke '/netzke', :controller => 'admin/netzke' |
Specifying a controller in the netzke route is new functionality. I'm now pointing to my own controller. Let's create it.
The controller
class Admin::NetzkeController < AdminController include Netzke::Railz::ControllerExtensions skip_before_filter :require_admin_user, :only => [:ext] private def require_admin_user unless admin_user_signed_in? unless login_related(params) render :status => :forbidden, :text => "Not logged in" end end end def login_related(params) (params[:netzke] && params[:netzke][:endpoint] == 'requireLogin') || (params[:endpoint] == 'deliverComponent' && params[:data] && params[:data][0] && params[:data][0][:args] && params[:data][0][:args][:name] == 'login') || (params[:path] == 'admin__login__login_form' && params[:endpoint] == 'netzkeSubmit') end end |
The skip_before_filter is in place so that the required javascript files to load my application can always be loaded. The database is never accessed through the ext action of the NetzkeController.
I return a status forbidden (which is a 403 error) if the user is not authenticated and catch that status on the client side. I'll show that later.
Furthermore, there are 3 login-related calls that I don't want to authenticate. The first one is an endpoint I use to call the login form when I load my app, the second delivers the login component and the last one submits the login form. The endpoint is situated in my main admin component and looks like this:
endpoint :require_login do |params, this| this.show_login_form unless Netzke::Base.controller.admin_user_signed_in? end |
The file admin/javascripts/admin.js looks like this:
{ initComponent: function() { this.callParent(); // handle server exceptions Netzke.directProvider.on('serverexception', function(self, e) { switch(self.xhr.status){ case 403: this.showLoginForm(); break; case 500: this.netzkeFeedback('Internal server error. The systems administrator is notified.') break; default: console.log('Error code: '+self.xhr.status); } console.log(self.xhr.status); }, this); this.requireLogin() }, showLoginForm: function() { var menuComponents = this.netzkeGetComponent('menu').componentsBeingLoaded for(component in menuComponents) { var storedConfig = menuComponents[component] if (storedConfig.loadMaskCmp) { storedConfig.loadMaskCmp.hide(); storedConfig.loadMaskCmp.destroy(); } delete this.componentsBeingLoaded[component]; } this.netzkeLoadComponent('login', {callback: function(l) { this.netzkeFeedback('You have to be logged in to access this page.') l.show(); }, scope: this}); } } |
Now, this is actually where most of the magic happens. The this.requireLogin() is called when the component is initialized and makes sure the login screen is shown, but only if the user isn't already logged in (which could happen on reload, for instance). The check for this takes place in the beforementioned endpoint.
I also added some exception handling to the initComponent function. This makes sure that whenever a 403 error is returned, the showLoginForm() function is called. And when the status code returned equals to 500, it will be logged in Airbrake. The default is there for debugging purposes.
The showLoginForm() function also incorporates a technique to ensure that if I'm loading a new component from the menu, the loadMask first disappears before showing the login form and it deletes the component from the componentsBeingLoaded.
Now, my login component is quite simple and it includes a form. Here is app/components/login.rb:
class Login < Netzke::Basepack::Window js_configure do |c| c.width = 400 c.height = 150 c.modal = true c.title = 'Login' c.closable = false end def configure(c) super c.items = [:login_form] end component :login_form do |c| c.klass = Login::Form end end |
And here is app/components/login/form.rb
class Login::Form < Netzke::Basepack::Form js_configure do |c| c.mixin end def configure(c) super c.items = [ { name: :email }, { name: :password, input_type: 'password' } ] end endpoint :netzke_submit do |params, this| data = ActiveSupport::JSON.decode(params[:data]) if(authenticate_user(data)) this.netzke_feedback('You are now logged in!') this.close_login_window else this.netzke_feedback('E-mail or password incorrect.') end end action :apply do |c| c.icon = :key_go c.text = I18n.t('components.login.form.button') end def authenticate_user(data) admin_user = AdminUser.find_by_email(data['email']) if admin_user && admin_user.valid_password?(data['password']) Netzke::Base.controller.sign_in :admin_user, admin_user return true else return false end end end |
app/components/login/form/javascripts/form.js:
{ closeLoginWindow: function() { loginComponent = this.ownerCt; menuComponent = Ext.ComponentManager.get(loginComponent.menuId); loginComponent.close(); menuComponent.loadComponent({cmp: 'dashboard', skipFocus: true}); } } |
And now we have a fully functional login sequence built into our Netzke app!