OK, I just found out something interesting about ruby 1.9.2. They've changed the default LOAD_PATH to not include "." (current directory). What's this actually mean? Well, for starters just about every example that I and most others have written will no longer work. Here's an example with something from the current Ramaze prototype (generated when you do a ramaze create foo
require 'model/user'
this will generate errors if you run it with ruby 1.9.2. What you'll need to do instead is either
__DIR__('user')
or
require_relative 'user'
The first version is compatible between 1.9 and 1.8 and the 2nd is 1.9 only. Both examples assume that the file this code is in is already in the model directory.
I should also note that Lee Jarvis has fixed the problem with the prototype already and it should be in the next update of Ramaze.
Like I said, most of my examples from previous posts are now wrong, but they should be pretty easy to fix once you know how. If you have any problems, be sure to let me know and I'll try to help get them worked out with you.
A few days ago I saw this post on creating a pastie "clone" using Sinatra and Datamapper. I thought that it might be cool to try something similar in Ramaze and Sequel. It actually ended up being pretty easy (and similar to the other post (especially since I "stole" most of his CSS, etc.)), so I thought I'd share it here. If you've read much of this blog, then you should be pretty familiar with most of this, so I won't go through all of the code here but just hit the highlights. Here's the controller, controller/main.rb:
# controllers/main.rb # # The MainController has methods for index/main (to create a new snippet) and show (to show # an existing snippet).
# The main/index page that shows a text area where we can put in a # title and a snippet. We'll grab the title and snippet text, create a new # snippet, and then go to the snippet display page to see it. def index @title = "Snippet!"
# We've got a post, so grab the title and the snippet and then create # a new Snippet with them and the current time. We'll then go ahead and # redirect to the show method with the id that we get from the snippet. if request.post? title = request[:title] snippet_text = request[:snippet] snippet = Snippet.create(:title => title, :body => snippet_text, :created_at => Time.now) redirect rs(:show, snippet.id) end end
# The show page that shows an existing snippet. It takes the id of an existing # snippet and if it exists shows it nicely highlighted. If it doesn't exist, we'll # just go back to the main page after setting the flash. def show(id) @title = "Show Snippet!"
# Find the snippet if it exists @snippet = Snippet[id]
if @snippet != nil # Get some text we can use to substitute for the "[/code]" text so that it doesn't # mess up Syntaxi. replacer = Time.now.strftime('[code-%d]')
# Do the syntax highlighting of the text with Syntaxi after we've done our # substitution. @snippet_highlight = Syntaxi.new("[code lang='ruby']#{@snippet.body.gsub('[/code]', replacer)}[/code]").process
# Substitute the '[/code]' back in for our replacement text. @snippet_highlight = "#{@snippet_highlight.gsub(replacer, '[/code]')}" else # The snippet doesn't exist, so set a message and just redirect # to the main/index page. flash[:message] = "Snippet #{id} not found." redirect rs :index end end end
We start out requiring syntaxi which can be used with a sudo gem install syntaxi. The next three lines set a few variables which can be read about at the link. The index method will all the user to put in a snippet in a text area and then we'll create the Snippet (see model/models.rb and dbMigration/001_RamazeSnippet.rb for the information in this table/model). After we've created the new snippet, we'll redirect to the show method with the id of the new snippet.
The show method takes the id of the snippet and then displays the syntax highlighted version of it. It grabs the snippet from the database and if the snippet is not nil, it creates a "replacer" for [/code] which signals syntaxi to quit highlighting. We then substitute this replacer for the a [/code] and process the snippet. When this is complete, we resubstitute the [/code] for replacer and save it so the view can get to it. The view/show.xhtml is pretty simple and just displays the title, the highlighted snippet, and the creation date.
Like I said, not too much other than the syntax highlighting that we haven't seen before here. You can check all of the code on GitHub and run the code on Heroku.
Over on the Ramaze list, we had quite a conversation going on about the MVC paradigm. In response to that, I ended up doing some research on partial rendering (rendering a piece of a view using another view). I ended up writing some code and it's available on GitHub here. The original code was generated using ramaze create RenderPartial and then modified to its present form.
class MainController < Controller # the index action is called automatically when no other action is specified def index @title = "Welcome to Ramaze!" end
The colors are predefined here in the controller, but in a real example, you would normally get them from a model probably from a database. The use of colors itself comes from the question in the email trail about dolls and the different color of eyes they could be. There's nothing particularly complex in here, we've seen it quite a few times, there's an index method and then two additional page methods where the latter are pretty much exactly the same.
The views are where the interesting part is though. Here's the page 1 view contained in view/page_1.xhtml
<p> Page 1 #{ render_partial :color } </p>
and the page 2 view, view/page_2.xhtml
<p> Page 2 #{ render_partial :color } </p>
and finally, the view/color.xhtml, the partial that we render:
<ul> <?r @colors.each do | color | ?> <li> #{color} </li> <?r end ?> </ul>
Here, we simply take the colors that were defined in the controller methods and use them to generate a list (obviously, we could have done whatever we wanted with them). The lines in the two page views #{ render_partial :color } tells the view to fill in this spot with the view/color.xhtml code. Although, we haven't here, you can also pass parameters to the partial. Here's a post that shows how to use that feature.
Hopefully, this is all pretty clear, but if not, as always, feel free to leave questions in the comments section.
Well, this turned out to be a bit harder than I expected, but with quite a bit of help from the Sequel, Ramaze, and Heroku lists, I got it worked out. The source code for this particular exercise is here. I'm only going to show a couple of files here as everything else you've already seen. First, here's how to do the migration in a rakefile ...
namespace :db do require 'rubygems' require 'sequel' Sequel.extension :migration
task :migrate do m = Sequel::Migrator db = Sequel.connect(ENV['DATABASE_URL'] || 'sqlite://library.sqlite') dir = "dbMigration"
m.run(db, dir, :target => target, :current => current) end end
We create a sequel migrator and connect to the database. The database connection line contains two parts. The first side is if there's an environment variable named DATABASE_URL then we'll use that to connect to the database. This is an environment variable set by Heroku. If this isn't set, then we'll go ahead and connect to a sqlite database, library.sqlite. We use the Sequel.connect command rather than Sequel.sqlite as we don't know which type of database we're going to be using. On Heroku, it will be PostgreSQL and locally, we use sqlite. Next up, we set the directory where we'll keep our migrations, in this case, dbMigration which is my standard. After that, there's two lines used for telling sequel which version we're going to, target, and starting from, current. Finally, we run the migrator with the database, directory, target and current. The final two are part of a hashtable that's used by this migrator. You could also use m.apply where the first two parameters are the same and then pass the target and current as integers, but the migrator code seems to imply that the run version is preferred. To run this locally, simply do a rake db:migrate and it should migrate to the latest version. To move to a different version you can run rake TARGET=0 db:migrate to remove everything from the database. If you just want to move to the latest version, don't put a target or current in, they'll be nil, and it should just migrate to the latest. Now, to run it on Heroku and migrate the database there simply add heroku in front of the command as in heroku rake db:migrate after you've pushed all the code up to Heroku with a git push heroku master as we saw in the last post.
I haven't talked about rake before, so I should probably give a little background for it. It's a ruby version of the Unix make. It has tasks and commands to do them. The homepage for it is here and here is a nice article on using it.
OK, now let's take a look at our model file model/models.rb.
# The database should have been set up using the database migrations in # the dbMigration directory. require 'rubygems' require 'ramaze' require 'sequel'
# Open the library's database. This must be done before we access the models # that use it. Sequel.connect(ENV['DATABASE_URL'] || 'sqlite://library.sqlite')
# # This is the model for the authors and is backed by the :authors table in the # database. # # Create the Author model. class Author < Sequel::Model many_to_many :books end
# # This is the model for the book and is backed by the :book table in the # database. # # Create the Book model. class Book < Sequel::Model many_to_many :authors end
This looks exactly like our previous models with a couple of exceptions. Here, we've moved our database connection into here from start.rb. It probably makes more sense here and keeps all of our Sequel code in one place (I got this idea from the Ramaze generated code). The connection itself looks surprisingly like our connection that was used in the rakefile. We end up using either a DATABASE_URL environment variable (supplied by Heroku) or the sqlite library.sqlite. After that, there are a couple of models that are in the database that we can use. The tables for the database and some data are created in the dbMigration/001_LibraryMigration.rb file.
I first heard of Heroku from, I believe, a rant by Zed (if you're offended by profanity, this is probably not for you). It looked pretty interesting and so I decided to give it a try. This was actually a bit more involved than I thought, not hard, but for the first time you have more than a few steps to get through. What I'm going to show is how to a) get Git running, set up GitHub, create the Ramaze application, load it to GitHub, and finally load it to Heroku. Strictly speaking, you don't need GitHub to get the app running on Heroku, but I'm going to start putting some of my posts up there and so I thought I might as well show it too.
Let's start with getting git up and running. On my Ubuntu system, simply type sudo apt-get install git-core. After that type git --version (my shows git version 1.7.2.1) to make sure everything installed correctly.
Next, let's set up a GitHub account. Go to http://github.com/plans and create yourself a free account (or move up to a paid account if you want). Next, you're going to need to generate a SSH key which will be used by both GitHub and Heroku. You can go to http://help.github.com/linux-key-setup/ if you're running Linux or http://help.github.com/windows-key-setup/ if you're on Windows (probably similar for you Mac users). Follow the directions and you should now have access to your GitHub account. Here's the command on Linux ssh-keygen -t rsa -C "yourusername@youremailprovider.com" to create the keys.
Now you'll want to configure git with a few your user name and email. The commands for this are: Configure git with user and email git config --global user.name "yourusername" git config --global user.email "yourusername@youremailprovider.com"
We're going to put our code up on GitHub first, so let's create an application first. Let's just use the default ramaze application and upload that (not exactly what I've done, but close enough). First go to your Dashboard on GitHub and create a new repository and call it "foo". This is where the application will live on GitHub. Now, run ramaze create foo to create an application called foo in the current directory. Then cd foo and we'll get the application ready to upload. We do need to add one file for Heroku and that's a .gems file. This file will contain a list of all the gems used by your application. In this case, the file should have the single line ramaze. If you're using Sequel say, the file would also contain a line sequel. In general it should have a line for each gem and the gem name should be what you would do by a sudo gem install xxx. Here we're going to run some git commands. I'm just learning git myself, but here's a book that should help you get started. The maintainer of this book, Scott Chacon, also has a dead tree book called "Pro Git" that is also quite good. So assuming we're in the foo directory already, type git init. This will get the application ready for git. Next we need to add the files with a git add . to add all the files in this directory and all of it's subdirectories into the repository. Now we need to do a "commit" of the files with a git commit -m 'first commit for foo'. Next, we'll tell git where we're going to save this with a git remote add origin git@github.com:slabounty/foo.git and finally, we'll push it to GitHub with git push origin master. You should now be able to go to your GitHub dashboard, see this repository and look at the files in there. Also, since this is a "public" repository, others will also be able to view and download these files, so don't use this for proprietary code unless you're using a non-free plan.
OK, let's move on to getting this up and running on Heroku. Start by creating an account on Heroku at http://heroku.com/. Once you have an account, you can sudo gem install heroku to get the Heroku gem. Next we'll do a heroku keys:add (this uses the same keys we created above to allow us to upload our files to Heroku. The one thing to make sure of is that you've created the .gems file as described above. Start with these two commands git remote add origin git@github.com:slabounty/SimpleHeroku.git git push origin master
Next we'll create an application on Heroku (we don't need to go to the website to do it, we'll just use the gem) with heroku create (creates an application on Heroku and let's you know where it's at. In my case it was http://smooth-stone-75.heroku.com) and finally we push it up to Heroku with a git push heroku master.
You can then point your browser where ever the application was created at, for example the web site above and you should see it running there. What is there is not the base Ramaze application that would be created but another simple one that I did. You should be able to see the code on GitHub.
There quite a bit of information here and if any of it is unclear, let me know.
Sorry for not posting recently. My trusty Sony VAIO passed away and I've been getting my new box, an HP dm3-1130us running Linux with all the tools that I normally use. I thought I'd document it in case anyone else needed to do it. To be honest, most of this should work on any system not just mine or even an HP. Here are the steps:
Create boot disks. Use the Recovery Manager.
Use http://unetbootin.sourceforge.net/ to create a bootable USB device.
Plug the USB drive in, reboot, and hit ESC (multiple times).
Select the USB device to boot
Ubuntu should load. Pick your name, language keyboard, time zone, and password (I think these are all that are asked for)
Install mailit mail package for ruby - sudo gem install mailit Install gemcutter (A replacement for SourceForge and gethub as gem repositories) - sudo gem install gemcutter
Be able to use matchit (match ruby do/end etc.) in vim (may want to look at vim-addons for Debian/Ubuntu) - set rtp+=/usr/share/vim/addons/ (in the .vimrc file)
Vimperator is a Firefox add-on that if you use vim, you absolutely need. It makes Firefox behave like vim ("j" scrolls down, "k" scrolls up, etc.). For vim itself, you need to look at snipmate. It provides snippets (similar to TextMate I believe) and it will make your Ruby programming life much easier. The final step, for matchit, is a bit of a hack and you probably want to investigate the vim-addons tool which from my quick glance, looked like a gem type thing for vim add ons.
I'm keeping a list of everything I do system wise on this machine. I discovered that for the most part I had a ton of stuff that I wasn't using and sometimes had no idea what it was.
So, let me know if you have any questions or comments.
This in from Jeremy Evans ------------------------------ Looks better, but I recommend a few more changes:
1) You need to salt your password hashes. Unsalted hashes are better than storing the password in plaintext, but most common hash algorithms probably already have large rainbow tables that will allow an easy lookup of most passwords given an unsalted hash.
2) I would recommend at least including random data when generating the random key for password resets. Your use of the username and Time.now makes it guessable if you know roughly when the user requested/will request a password change. encrypt_password is a poorly named method, since you are hashing, not encrypting (encrypting implies the possibility of decrypting, while hashing is one way).
3) I generally put a time limit on password resets. That way if someone requests one, but then remembers their password and doesn't change it, they are not vulnerable to someone else changing it next year. ------------------------------------------------
So, add the salt for the password, modify the generate_rand_key() to add a random component (possibly just add a #{rand(1000000)} to the end of the string to add in a 6 digit random component, rename encrypt_password to say hash_password, and finally add a time limit. Here, you'll need to add a date to the user table, add a date when you generate_rand_key(), and check this in change_password() when you also test the existence of a user with the key.
Thanks for the improvements Jeremy.
------------------------------------------------
After my last post, I received, as I noted, some rather stern comments on a) saving passwords in plaintext in the database and b) sending them in plaintext via email. So, I decided to fix the example up to make it a bit better. We'll be saving the password as a hash in the database and also when the user forgets their password, we'll send a link to let them change it. One other suggestion was to add a salt for the password hash which is easy enough to do, but I've left it as an exercise for the reader. I should note that some of the ideas in this post were stolen (and I mean that in the good sense) from JustKez. This post is definitely worth reading over as are others on the site.
OK, let's get started. We'll start out with the database migration.
# dbMigration/001_ForgotPassword.rb # # This is the "new" way to do migrations by using the Class.new form. # It means that a new class name is not required and so there is no chance # of creating a second class in the migration files that is the same as the # first causing problems. Class.new(Sequel::Migration)do def up # Create the users table. create_table(:users)do primary_key:id String:user_name String:password String:email String:rand_key end
end
def down # Remove the two tables. drop_table(:users) end end
This is a bit simpler than in the previous example. We're only going to have a users table with the user_name, password (which will be encrypted), an email address, and a random key (used for sending the user their password). The down method will just drop the users table. To generate the password, simply type sequel -M dbMigration/ sqlite://forgot.db. This will create the database we'll use for the project.
The model for this is pretty simple too.
# models/models.rb # # Create the User model. Each user can have a single challenge question. require'digest/sha1' class User<Sequel::Model
# Return an encrypted password based on the one passed in. def self.encrypt_password(password) Digest::SHA1.hexdigest(password) end
# Generate a random key based on the username and the current time in # rand_key and save the model back to the database. We'll use this to email # the user so that they can changer their password. def generate_rand_key self.rand_key=Digest::SHA1.hexdigest("#{@username} -- #{Time.now}") save end end
The only model we have is for the User. We have a couple of methods. The first is a Class level method for encrypting the password. We create a hash of the password using SHA1 and return the value (it will be used for creation of the user and changing the password). The second method is for generating a random key which will be passed as part of a link to the user in case they forget or lose their password.
Here's our start code
# start.rb # # This is the main program for the example. It loads the Sequel database, # loads the controllers and models, and then starts up Ramaze. # # The database should have been set up using the database migrations in # the dbMigration directory. require'rubygems' require'ramaze' require'sequel'
# Required for mailing. require'net/smtp' require'mailit'
# Create the mailer. MAILER=Mailit::Mailer.new(:server=>'MailServer.MyCompany.COM',:port=>25,:username=>'ApplicationName', :password=>'ApplicationPassword')
# Open the forgot password database. This must be done before we access the # models that use it. DB=Sequel.sqlite("forgot.db")
# Load the controllers and models. require'models/models' require'controllers/main_controller'
# Start Ramaze. Ramaze.start
This is pretty much the same as most of our start files. We add in the code for mailit, open the database, and then require our models and controllers. Finally, we start ramaze.
Next up our only controller.
# controllers/main_controller.rb # # The mainController has a single method index. First map to / for # the view. Next, set the layout to page so that we use the layout/page.xhtml for our # layout. class MainController<Ramaze::Controller
# The Main controller will be accessed using "main" as in: # http://localhost:7000/main. map'/'
# Use page.xhtml in the layout directory for layout except # for when we're doing AJAX. layout(:page){!request.xhr?}
# Set up a helper to check if we're logged in and only allow access # to the :logged_in page if we are. This is probably the hard way to # do this for only the single page but will make much more sense if # we add more pages as we'd do in a real application. helper:aspect before(:account_settings,:main){ unlesslogged_in? # Set the flash message which will only be available in the next # screen. In this case that will be the logged_in screen. flash[:message]="You must log in before accessing the requested page." redirectrs(:index) end }
# You can access it now with http://localhost:7000/ def index @title="SteamCode - User" end
# Placeholder for real content. def main "<h2>Main</h2>" end
# Placeholder for real content. def about "<h2>About</h2>" end
# Placeholder for real content. def help "<h2>User Help</h2>" end
# Register a new user with SteamCode. We will get here from the # views/register.xhtml page where the user will put in their (requested) # login, password, and email address. First find if the user already # exists, if it does, then we'll set a message to tell the user so and # redirect them back to the register screen. If not, we'll go ahead and add # them to the database with the appropriate login, password, and email # address. We'll then send them to the login screen to let them log in to # SteamCode. def register @title="Register with SteamCode" # Make sure we're getting here from a post request. ifrequest.post? # Check the login and password. # if we find the Account based on the login and password. If we find it # we'll save the login ID in the session variable and we can use that # to show if the Account is currently logged in or not. If we can't # find the Account, we'll set the flash message, set the session to nil # and just stay on this page. unlessUser.find(:user_name=>request[:user_name]) # This account does not exist. Grab the user_name, the password, # and the email and create a new Account with them. user_name=request[:user_name] password=request[:password] email=request[:email]
# Create the account with the user_name, password, and email given. user=User.create(:user_name=>user_name,:password=>User.encrypt_password(password),:email=>email) Ramaze::Log.debug"New User Added: user_name = #{user.user_name} password = #{user.password} email = #{user.email}"
# Redirect to the login page. redirectrs(:login)
else # This user already exists. Set the flash message for them to # try again. flash[:message]="Login #{request[:user_name]} already used. Please select another."
# Stay on the register page. redirectrs(:register) end end end
# Login to Steamcode. If the request is a post, then we'll try to find the # user. If we succeed then we'll set some session variables and redirect to # the main page (which the user can only access if they're logged in). If they # can't be logged in, we'll set a flash message, reset the session, and redirect # back to the login page. def login @title="Login to SteamCode" ifrequest.post? ifuser=User.find(:user_name=>request[:user_name],:password=>User.encrypt_password(request[:password])) # Use the name= portion of the input form to grab the data # from the request variable and save it in the session # hash table. session[:user_id]=user.id session[:user_name]=user.user_name
# Redirect to the main screen. redirectrs(:main) else # The login could not be authorized. Set the flash message # and stay on this page (index/login). Set the session loginID # to nil also. This will effectively log the user out. This would # be reasonable if they are logged in and then try to log in with # a new login/password. flash[:message]="Incorrect user name or password, please try again!!!" session[:user_id]=nil session[:user_name]=nil
# Stay on the login page. redirectrs(:login) end end end
# Let the user change account settings. For now this is # just the email and password. def account_settings @title="Account Settings for SteamCode" user=User[session[:user_id]] ifrequest.post? user=User[session[:user_id]] user.email=request[:email] user.password=User.encrypt_password(request[:password]) user.save flash[:message]="New email and/or password saved." redirectrs(:main) end @current_email=user.email end
# Logout of the system. Set the flash message and then # set the session values to nil. Finally, redirect back to the # index page. def logout @title="Logout from SteamCode" flash[:message]="#{session[:user_name]} Logged out" session[:user_id]=nil session[:user_name]=nil redirectrs(:index) end
# The user has requested that we email their password back to them. Generate # a random key and email it to them to allow them to change their password. def forgot_password ifrequest.post? # Final submit. ifuser=User.find(:user_name=>request[:user_name])
# Generate a key and associate it with the user_name. user.generate_rand_key
# Create the mail message and fill it in with the appropriate # information (to/from/subject/text). Then send it off. mail=Mailit::Mail.new mail.to=user.email mail.from="Steamcode@MyCompany.com" mail.subject="Steamcode Password" mail.text="To change your password: http://localhost:7000/change_password/#{user.rand_key}"
# Send the mail message via the MAILER (created in start.rb). MAILER.send(mail)
# Just go back to the login page. redirectrs(:login) else flash[:message]="Could not find user: #{request[:user_name]}"
# Could not find user with this user_name. redirectrs(:forgot_password) end end end
# The key will be the key that we created above when the user asked # for the password change. It will be passed as a parameter when the user # clicks on the link that was emailed to them. def change_password(key) # Check to make sure that there's a user with this key. user=User.find(:rand_key=>key) ifuser ifrequest.post? # We got here from a post and there's a user with the key. Go ahead # and a) change the password, b) reset the random key, and c) save # the new user information. Then set the flash message and put them # on the login screen. user.password=User.encrypt_password(request[:password]) user.rand_key=nil user.save flash[:message]="Your new password saved." redirectrs(:login) end else # There wasn't a user with this random key. Just flash a message # and then redirect to the login screen. flash[:message]="This does not appear to be a valid request." redirectrs(:login) end end
private
# If the user is logged in, the session will # contain a non nil user id. def logged_in? session[:user_id]!=nil end end
This is very similar to many of our other controllers from our past posts. We map to '/', Add the layout (which won't be used for AJAX calls (not used here anyway)), and then the aspect which will prevent the user from going to certain pages unless they're logged in to the system. The next four methods, index, main, about, and help are really just place holders. Index is where the user will end up initially and main is where they will end up after logging in. Then we have the register page. This will check if it's a post and if it is, try to find the user. If it can't find the user (note the use of "unless" here. To a certain extend, I'm trying to decide if it helps or hinder readability. Let me know what you think) it will create a new one with the parameters from the request hash. If the user already exists, we'll just flash a message back and redirect them back to the register page. Next we have the login method. We check to make sure this is from a post, then check to see if we can find the user based on their user name and password (using the model's encrypt_password method). If we find them, we go ahead and log them in (set the session variables) and redirect them to the main page. If not, we flash a message, reset the session variables, and redirect them back to the login page. The account_settings page let's the user reset their email and password. The logout page resets the session variables and then redirects back to the index page. Looking at it now, I'd recommend refactoring the session resets here and in login to their own private method (once again we'll leave that as an exercise). The forgot_password method is the reason for all of this. We check that it's a post and contains a valid user. If so, we generate a random key for the user (using the model method) and then create an email with the link containing this key. If we can't find the user, we'll flash a message and redirect back to the same page. Finally (for the pages anyway), we have the change_password page. This checks to see if we have user with this rand_key (from the link generated above) and if we do, we check if this is a post and change their password appropriately, reset the random key (so it's used only once), flash a message and redirect them back to the login page. If there wasn't a user with that random key, we simply flash a message and redirect them back to the login page. Finally, we have the private method that's used to check if a user is logged in to the system.
<htmlxmlns="http://www.w3.org/1999/xhtml"> <head> <!-- Use the page.css in the public directory and set title based on what's set in the associated method. --> <linkrel="stylesheet"type="text/css"href="/page.css"/>
<divid="nav"> <!-- Main/User controller --> <!-- Move this next section over to the right side of the screen. It will contain the Login/Register if we're not logged in and the Logout if we are. --> <spanstyle="float: right"> <!-- We're going to use the private method logged_in? here to test if we want to show the Login/Register links or the Logout link --> <?rif !logged_in??> <ahref="#{r(:login)}">Login</a> <ahref="#{r(:register)}">Register</a> <?relse?> <ahref="#{r(:account_settings)}">Account</a> <ahref="#{r(:logout)}">Logout</a> <?rend?> </span>
<!-- These next three will be on the left side and always there --> <ahref="#{r(:index)}">Home</a> | <ahref="#{r(:about)}">About Us</a> | <ahref="#{r(:help)}">Help</a> </div>
<divid="content"> <!-- Display the actual content. This will come from the method or the associated view/*.xhtml file --> #@content </div>
<!-- Set the footer in the center of the screen. --> <divid="footer"style="text-align: center;"> <h5> Powered by Ramaze </h5> </div> </div> </body> </html>
It's stripped down a bit from the last post and has all of the administration stuff removed. Since you've seen this in past posts, I won't go through it. It's mostly just code for the navigation with some content and a header and footer tossed in for good measure.
Let's take a look at the views for the project. They're really simple this time. No JavaScript and no AJAX. Here's the index.
#{flashbox} <h2>Welcome</h2>
Here's the login page
#{flashbox} <ahref="#{r(:forgot_password)}">Forgot your password?</a> <br/>
<formid="login"method="post"> <fieldset> <legend> Login </legend> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the user_name. --> <labelfor="user_name">User Name:</label> <inputid="user_name"name="user_name"type="text"/> <br/>
<!-- Input for the user_name. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Submit the login request. --> <inputtype="submit"value="Login"/> </div> </fieldset> </form>
There's just input boxes for the user name and password and a button for submitting.
Here's the registration page.
<!-- view/register.xhtml --> <formid="register"method="post"> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the user_name. --> <labelfor="user_name">Login:</label> <inputid="user_name"name="user_name"type="text"/> <br/>
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Input for the email. --> <labelfor="email">Email:</label> <inputid="email"name="email"type="text"/> <br/>
<!-- Submit the new User --> <inputtype="submit"value="Register"/> </div> </form>
As above boxes for user name, password, and email plus the submit button.
Here's the page for changing the account settings.
<!-- Let's the user change their email and/or password --> <formid="change_password"method="post"> <fieldset> <legend> Change Settings </legend> <div> <!-- Input for the email address. --> <labelfor="email">Email:</label> <inputid="email"name="email"type="text"value=#{@current_email}/> <br/>
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Submit the new email and/or password --> <inputtype="submit"value="Submit"/> </div> </fieldset> </form>
This contains boxes for password and email (the user can't change their user_name) and the submit button.
<!-- for= goes with id=, the name= is placed in the request variable. --> <!-- Input for the user_name. --> <labelfor="user_name">User Name:</label> <inputid="user_name"name="user_name"type="text"value="User Name"/> <br/>
</div>
<!-- Once they've put in the challenge answer, they will select --> <!-- this and the system will send their password via email. --> <inputtype="submit"value="Send Password"/> </fieldset> </form> <br/>
This has a box for the user_name and the submit button. When the user puts in their user_name and submits, an email is generated (see above) that provides a link to change their password. This page is the change_password.xhtml.
<!-- view/register.xhtml --> <formid="change_password"method="post"> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Submit the new User --> <inputtype="submit"value="Change Password."/> </div> </form>
Once again, dead simple with the password box and a submit (you'd probably want a confirmation password that would be checked using JavaScript, but we'll just assume that the user won't make any mistakes.
That's pretty much everything except for the CSS which is in public/page.css
So, I hope this works out for everyone a bit better than the last version and gets at least a bit closer to what you might use in a "real" project. Let me know if you have questions or comments.
Edit: I received some rather stern comments on this post on the Sequel mail list and probably rather deservedly. The primary complaints were that you really shouldn't even be doing this sort of thing as it a) involves saving the user's password as plaintext in the database and b) sending the user's password in plaintext over the internet via email. The solution is to save the user's password as a hash in the database and then provide a link to them for changing the password if requested. I basically agree with the criticism, so use this only for non-critical applications (i.e. don't use if for a banking app.) or better yet, just use some of the techniqes (email, AJAX) without using the whole idea. Thanks to all who commented to set me straight.
For a recent project, I was looking at how to notify the user if they forgot their password. I'd initially thought the interesting piece would be the email part, but really, not so much. Let's start there though. The first thing you'll need to add that we haven't before is install Michael Fellinger's (Manveru) Mailit. So ... sudo gem install mailit
With that taken care of, let's take a look at our database migrations. There's two of them since I added the admins table later. Here's the first one
# dbMigration/001_ForgotPassword.rb # # This is the "new" way to do migrations by using the Class.new form. # It means that a new class name is not required and so there is no chance # of creating a second class in the migration files that is the same as the # first causing problems. Class.new(Sequel::Migration)do def up # Create the users table. create_table(:users)do primary_key:id String:user_name String:password String:email String:challenge_answer foreign_key:challenge_question_id,:challenge_questions end
# Create the challenge questions table. create_table(:challenge_questions)do primary_key:id String:question end
end
def down # Remove the two tables. drop_table(:users,:challenge_questions) end end
Here we create a user table that has a user_name, password, email, and a challenge_response. It also has a foreign key to the challenge_questions table for the challenge question. The challenge_question table contains a list of potential questions that the user can select. This will be handled as a drop down when they register for the site.
The second migration script contains the admins table. For this application, about the only thing an admin can do is add more challenge questions. We'll also add in an admin since we don't have another way of doing it in this application. Here we'll give the admin the name "admin" (clever) and a password "helloworld" (better anyway), and an email address that's never used.
# dbMigration/002_ForgotPasswordMigration.rb # # This is the "new" way to do migrations by using the Class.new form. # It means that a new class name is not required and so there is no chance # of creating a second class in the migration files that is the same as the # first causing problems. Class.new(Sequel::Migration)do def up # Add in the admins table for administrators. create_table(:admins)do primary_key:id String:admin_name String:password String:email end
# Add in an administrator. from(:admins).insert(:admin_name=>'admin',:password=>'helloworld',:email=>'admin@company.com')
end
def down # Remove the administrators table. drop_table(:admins) end end
We run our migrations with the following:
sequel -m dbMigration/ sqlite://forgot.db
The models we use for this project are pretty simple. The User model just states that it's many_to_one with the challenge questions (many users can have the same challenge question). The Admin model is about as simple as it gets (empty). The ChallengeQuestion model has the one_to_many with users (one challenge question can be associated with many users). It also has the self.questions method which will return all of the challenge questions in the database and their associated ids. This will be used by the registration page to allow the user to select one challenge question from a drop down.
# Create the User model. Each user can have a single challenge question. class User<Sequel::Model many_to_one:challenge_question end
# Create the Challenge_Question model. Multiple users can have a single # challenge question. class ChallengeQuestion<Sequel::Model one_to_many:users
# Will return an array of all of the questions. def self.questions select(:id,:question).all end end
# Create the Administrator model. class Admin<Sequel::Model end
So now we're ready to move on to the start.rb file.
# start.rb # # This is the main program for the example. It loads the Sequel database, # loads the controllers and models, and then starts up Ramaze. # # The database should have been set up using the database migrations in # the dbMigration directory. require'rubygems' require'ramaze' require'sequel'
# Required for mailing. require'net/smtp' require'mailit'
# Create the mailer. MAILER=Mailit::Mailer.new(:server=>'MailServer.MyCompany.COM',:port=>25,:username=>'ApplicationName',:password=>'ApplicationPassword')
# Open the forgot password database. This must be done before we access the # models that use it. DB=Sequel.sqlite("forgot.db")
# Load the controllers and models. require'models/models' require'controllers/main_controller' require'controllers/admin_controller'
# Start Ramaze. Ramaze.start
The difference here from most of our other start.rb files is the addition of the code for the mailer. First we require 'net/smtp' and 'mailit'. The we create the MAILER. We'll pass the server name, the port (will almost certainly be 25), the username, and the password. This will create a mailer that we can then "send" messages to. Next we open the database, forgot.db, and then get the models and the controllers.
Let's take a look at the admin controller first.
# controllers/admin_controller.rb # # The AdminController has a single method index. First map to /admin for # the view. Next, set the layout to page so that we use the layout/page.xhtml for our # layout. Then we use a helper for the :xhtml which will allow us to use "js" in the # layout (we use this to generate a javascript link). class AdminController<Ramaze::Controller # The Admin controller will be accessed using "admin" as in: # http://localhost:7000/admin. map'/admin'
# Use page.xhtml in the layout directory for layout layout:page
# Let's us put in our "js" lines. helper(:xhtml)
# Set up a helper to check if we're logged in and only allow access # to the :logged_in page if we are. This is probably the hard way to # do this for only the single page but will make much more sense if # we add more pages as we'd do in a real application. helper:aspect before(:main,:add_challenge){ unlesslogged_in? # Set the flash message which will only be available in the next # screen. In this case that will be the logged_in screen. flash[:message]="You must log in before accessing the requested page." redirectrs(:index) end }
# You can access it now with http://localhost:7000/admin def index Ramaze::Log.debug"Enter Admin Index" @title="SteamCode - Administration Home" end
def main end
# Add a new challenge question for the user to select. def add_challenge ifrequest.post? challenge_question=request[:challenge_question] ChallengeQuestion.create(:question=>challenge_question) end end
# Login as an administrator. Figure out if this is a correct login/password # pair and log the admin in and redirect ot main if it is. If not, flash # a message and redirect back to the login page. def login @title="Library - Administration - Login" ifrequest.post? ifadmin=Admin.find(:admin_name=>request[:admin_name],:password=>request[:password]) # Use the name= portion of the input form to grab the data # from the request variable and save it in the session # hash table. session[:admin_id]=admin.id
# Redirect to the list_all screen. redirectrs(:main) else # The login could not be authorized. Set the flash message # and stay on this page (index/login). Set the session loginID # to nil also. This will effectively log the user out. This would # be reasonable if they are logged in and then try to log in with # a new login/password. flash[:message]="Incorrect user name or password, please try again!!!" session[:admin_id]=nil
# Stay on the login page. redirectrs(:login) end end end
# Log the administrator out. def logout session[:admin_id]=nil flash[:message]="Admin Logged out" redirectMainController.r(:index) end
private
# If the admin is logged in, the session will # contain a non nil admin id. def logged_in? session[:admin_id]!=nil end
end
It starts out with most of the same code as all of our controllers. There's the map command, the layout (here we'll use layout/page.xhtml), the helper for our javascript, the helper for aspect (this will allow us to do before/after commands for selected methods in this controller), the before command which will have us check for being logged in before allowing access to the main page and the add_challenge page. The first method is the index method which is the page that the admin will go to before logging in. This is followed by main which is where the admin will be redirected after login. Next we have the add_challenge method. Here we just grab the question from the request hash (filled in by the add_challenge page input) and create a new ChallengeQuestion from it (also adding it to the database). Next is the login method, which should be pretty familiar from past posts. If we find the admin (and here the only one we have was created by the database migration), we'll set the session admin_id variable and redirect to the main page. If we fail to login the admin, we'll put up a flash message and redirect back to the login page. Next is the logout which just resets the session admin_id, sets a message, and redirects back to the login page. Finally, we have the private method logged_in? which will check if an admin is logged in.
Let's take a look at the admin views. First we have the view/admin/index.xhtml.
#{flashbox} <p> Welcome to the Library. This is the Administrator's section. Please login and administrate. </p>
Nothing too much here, just a note for the user to login.
Next we have the main view.
<!-- The only thing here is a link to add a challenge question. --> <h2>Enter Admin</h2> <ahref="#{r(:add_challenge)}">Add Challenge Question</a>
This is the page that the admin will see when they log in to the system. Here, there's just a link to the add challenge question page.
<formid="login"method="post"> <fieldset> <legend> Add Challenge </legend> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the challenge_question. --> <labelfor="challenge_question">User Name:</label> <inputid="challenge_question"name="challenge_question"type="text"/> <br/>
<!-- Submit the new challenge question (this should result in it being saved in the database) --> <inputtype="submit"value="Add"/> </div> </fieldset> </form>
Here we just have the input box for the challenge question and the submit button. This will, as noted above, add a new challenge question to the database.
Here's the login page.
#{flashbox} <formid="login"method="post"> <fieldset> <legend> Login </legend> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the admin_name. --> <labelfor="admin_name">Admin:</label> <inputid="admin_name"name="admin_name"type="text"/> <br/>
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Submit the admin name and password. If accepted, the admin should get logged in. --> <inputtype="submit"value="Login"/> </div> </fieldset> </form>
It has input boxes for the admin's admin_name and password as well as the submit button. Once again, nothing very interesting. And the end of the admin pages.
Let's turn to the user side now. First we have the user controller, controllers/main_controller.rb.
# controllers/main_controller.rb # # The mainController has a single method index. First map to /admin for # the view. Next, set the layout to page so that we use the layout/page.xhtml for our # layout. Then we use a helper for the :xhtml which will allow us to use "js" in the # layout (we use this to generate a javascript link). class MainController<Ramaze::Controller
# The Main controller will be accessed using "main" as in: # http://localhost:7000/main. map'/'
# Use page.xhtml in the layout directory for layout except # for when we're doing AJAX. layout(:page){!request.xhr?}
# Let's us put in our "js" lines. helper(:xhtml)
# Set up a helper to check if we're logged in and only allow access # to the :logged_in page if we are. This is probably the hard way to # do this for only the single page but will make much more sense if # we add more pages as we'd do in a real application. helper:aspect before(:account_settings,:main){ unlesslogged_in? # Set the flash message which will only be available in the next # screen. In this case that will be the logged_in screen. flash[:message]="You must log in before accessing the requested page." redirectrs(:index) end }
# You can access it now with http://localhost:7000/ def index @title="SteamCode - User" end
# Placeholder for real content. def main "<h2>Main</h2>" end
# Placeholder for real content. def about "<h2>About</h2>" end
# Placeholder for real content. def help "<h2>User Help</h2>" end
# Register a new user with SteamCode. We will get here from the # views/register.xhtml page where the user will put in their (requested) # login, password, and email address. First find if the user already # exists, if it does, then we'll set a message to tell the user so and # redirect them back to the register screen. If not, we'll go ahead and add # them to the database with the appropriate login, password, and email # address. We'll then send them to the login screen to let them log in to # SteamCode. def register @title="Register with SteamCode" @questions=ChallengeQuestion.questions # Make sure we're getting here from a post request. ifrequest.post? # Check the login and password. # if we find the Account based on the login and password. If we find it # we'll save the login ID in the session variable and we can use that # to show if the Account is currently logged in or not. If we can't # find the Account, we'll set the flash message, set the session to nil # and just stay on this page. ifUser.find(:user_name=>request[:user_name])
# This user already exists. Set the flash message for them to # try again. flash[:message]="Login #{request[:user_name]} already used. Please select another."
# Stay on the register page. redirectrs(:register) else # This account does not exist. Grab the user_name, the password, # and the email and create a new Account with them. user_name=request[:user_name] password=request[:password] email=request[:email]
# Log the new user (a real application wouldn't probably print # the password out though). # Ramaze::Log.debug "New User Added: user_name = #{user_name} password = #{password} email = #{email}"
# Create the account with the user_name, password, and email given. user=User.create(:user_name=>user_name,:password=>password,:email=>email, :challenge_answer=>request[:challenge_answer], :challenge_question=>ChallengeQuestion[request[:challenge_question]]) Ramaze::Log.debug"New User Added: user_name = #{user.user_name} password = #{user.password} email = #{user.email} question = #{user.challenge_question.question}"
# Redirect to the login page. redirectrs(:login) end end end
# Login to Steamcode. If the request is a post, then we'll try to find the # user. If we succeed then we'll set some session variables and redirect to # the main page (which the user can only access if they're logged in). If they # can't be logged in, we'll set a flash message, reset the session, and redirect # back to the login page. def login ifrequest.post? ifuser=User.find(:user_name=>request[:user_name],:password=>request[:password]) # Use the name= portion of the input form to grab the data # from the request variable and save it in the session # hash table. session[:user_id]=user.id session[:user_name]=user.user_name
# Redirect to the main screen. redirectrs(:main) else # The login could not be authorized. Set the flash message # and stay on this page (index/login). Set the session loginID # to nil also. This will effectively log the user out. This would # be reasonable if they are logged in and then try to log in with # a new login/password. flash[:message]="Incorrect user name or password, please try again!!!" session[:user_id]=nil session[:user_name]=nil
# Stay on the login page. redirectrs(:login) end end end
# Let the user change account settings. For now this is # just the email and password. def account_settings user=User[session[:user_id]] ifrequest.post? user=User[session[:user_id]] user.email=request[:email] user.password=request[:password] user.save flash[:message]="New email and/or password saved." redirectrs(:main) end @current_password=user.password @current_email=user.email end
# Logout of the system. Set the flash message and then # set the session values to nil. Finally, redirect back to the # index page. def logout flash[:message]="#{session[:user_name]} Logged out" session[:user_id]=nil session[:user_name]=nil redirectrs(:index) end
# The user has requested that we email their password back to them. When they submit # their challenge response and we verify it, we'll set up an email response using # Mailit and send it to them. def forgot_password Ramaze::Log.debug"Enter Forgot Password" @page_javascript='forgot_password' ifrequest.post? Ramaze::Log.debug"Challenge Question Submitted: Email: #{request[:user_name]} Answer: #{request[:challenge_answer]}" # Final submit. ifuser=User.find(:user_name=>request[:user_name],:challenge_answer=>request[:challenge_answer]) Ramaze::Log.debug"Found user: #{user.user_name}#{user.email}#{user.password}"
# Create the mail message and fill it in with the appropriate # information (to/from/subject/text). Then send it off. mail=Mailit::Mail.new mail.to=user.email mail.from="Steamcode@MyCompany.com" mail.subject="Steamcode Password" mail.text="Your password is: #{user.password}."
# Send the mail message via the MAILER (created in start.rb). MAILER.send(mail)
# Just go back to the login page. redirectrs(:login) else Ramaze::Log.debug"Could not find user: #{request[:user_name]} or incorrect challenge response." flash[:message]="Could not find user: #{request[:user_name]} or incorrect challenge response."
# Could not find user with this user_name/challenge answer just redirect to forgot password redirectrs(:forgot_password) end end end
# This is called from an Ajax request. We take in the email address that the # user submitted and then pass back the challenge question for that user. If # we can't find the user, we won't respond with anything and we'll let the # javascript (public/js/forgot_password.js) deal with it. In this case, they'll # just pop up an alert to let the user know. def generate_forgot_question ifrequest.xhr? # Get the user_name and if it exists, return the challenge question. If not, generate the # could not find user_name messesage. ifuser=User.find(:user_name=>request[:user_name]) challenge_question=user.challenge_question.question Ramaze::Log.debug"Challenge Question Requested: challenge_question = #{challenge_question}"
# It looks like we a) MUST use the respond command and b)MUST use the 200 return value. This was # determined by just trying different things. json="{ challenge_question: \"#{challenge_question}\"}" respondjson,200 else # Go ahead and log a message. Ramaze::Log.debug"Could not find user with user_name: #{request[:user_name]}" end end end
private
# If the user is logged in, the session will # contain a non nil user id. def logged_in? session[:user_id]!=nil end end
The main controller starts the exact same way that the admin controller does. It has the map, layout, helper, and aspect lines and for all of the exact same reasons. Next is the index method which is the default page before someone logs in and this is followed by the main page which is where a user ends up after they log in. Next are two placeholder methods about and help that can be used for obvious purposes. Next is the registration method. We check if this is called from a post and if it is, we check to see if we already have a user with the user_name that was submitted. If we already have that name registered, we'll flash the user a message and send them back to the registration page. If we don't, we'll create a new user with the user_name, password, email, and challenge question/answer. Then we'll redirect them to the main page. Next, the login page will see if they can find the user with the given user_name and password and if so, we save their information in the session and redirect them to the main page. If not, we'll flash a message and redirect back to the login page so that they can try again. The account_settings allows the user to change their email and/or password. The logout method, like the corresponding admin logout, sets the session id and redirects the user back to the index page. The forgot_password method is the main reason for the post and it's actually pretty simple. We check the user_name and the challenge_answer that they provided and if they match we use the MAILER constant to send the email containing their password and redirect them to the login page. If we can't find the user or if the challenge answer doesn't match, we'll flash a message and send them back to the forgot_password page. The generate_forgot_question will come from an AJAX request. If we find the user_name, we'll send their challenge question back to them using JSON. If not, we'll just stay on the same page and they can try again. Finally, we have the logged_in? method, for checking if the user is logged in (obviously). We use this to protect certain pages from users who aren't logged in.
Now, let's take a look at the views. First is the index page and it's a simple Welcome message.
#{flashbox} <h2>Welcome</h2>
Next, the registration page contains input boxes for the user_name, password, email, and challenge response. There's also a drop down for the challenge question and the submit button.
<!-- view/register.xhtml --> <formid="register"method="post"> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the user_name. --> <labelfor="user_name">Login:</label> <inputid="user_name"name="user_name"type="text"/> <br/>
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Input for the email. --> <labelfor="email">Email:</label> <inputid="email"name="email"type="text"/> <br/>
<!-- Input for the challenge question. The register() method will get the list of challenge questions from the database table ChallengeQuestion and will pass the list of questions and ids. --> <labelfor="challenge_question"class="label">Challenge Question:</label> <selectname="challenge_question"> <?r @questions.eachdo | question | ?> <optionvalue=#{question.id}>#{question.question} </option> <?rend?> </select> <br/>
<!-- Input for the challenge_answer. We'll save this and if they need to retrieve their password, we'll ask the question from above and see if they know the answer they will submit here. --> <labelfor="challenge_answer">Challenge Response:</label> <inputid="challenge_answer"name="challenge_answer"type="text"/> <br/>
<!-- Submit the new User --> <inputtype="submit"value="Register"/> </div> </form>
The login page only has the input boxes for the user_name and password along with the submit button.
#{flashbox} <ahref="#{r(:forgot_password)}">Forgot your password?</a> <br/>
<formid="login"method="post"> <fieldset> <legend> Login </legend> <div> <!-- for= goes with id=, the name= is placed in the request variable. -->
<!-- Input for the user_name. --> <labelfor="user_name">User Name:</label> <inputid="user_name"name="user_name"type="text"/> <br/>
<!-- Input for the user_name. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"/> <br/>
<!-- Submit the login request. --> <inputtype="submit"value="Login"/> </div> </fieldset> </form>
The account_settings page only has the input boxes for the email and password along with the submit button. It would actually be nice to have a way to change the challenge question/response also.
<!-- Let's the user change their email and/or password --> <formid="change_password"method="post"> <fieldset> <legend> Change Settings </legend> <div> <!-- Input for the email address. --> <labelfor="email">Email:</label> <inputid="email"name="email"type="text"value=#{@current_email}/> <br/>
<!-- Input for the password. --> <labelfor="password">Password:</label> <inputid="password"name="password"type="password"value=#{@current_password}/> <br/>
<!-- Submit the new email and/or password --> <inputtype="submit"value="Submit"/> </div> </fieldset> </form>
The forgot_password page is reached from the login page. The user is offered a link for a forgotten password. Here they have an input box for their user name and then they'll put submit for and get their challenge question back. We then use a bit of AJAX magic to display the challenge question. When they put in their answer they can submit that and then get their password mailed to them as outlined above. Here's the XHTML followed by the JavaScript for the AJAX piece.
<!-- for= goes with id=, the name= is placed in the request variable. --> <!-- Input for the user_name. --> <labelfor="user_name">User Name:</label> <inputid="user_name"name="user_name"type="text"value="User Name"/> <br/>
<!-- Button for getting the challenge question based on the --> <!-- user_name above --> <divid='challenge_question'> <inputtype="submit"id="get_challenge"value="Get Challenge"/> </div>
</div>
<!-- Once they've put in the challenge answer, they will select --> <!-- this and the system will send their password via email. --> <inputtype="submit"value="Send Password"/> </fieldset> </form> <br/>
// public/js/forgot_password.js $(document).ready(function() { // Grab the get_challenge so we can add the choiceMarkup to it. var user_name_container = $("#user_name");
// Add click handler. When the get_challenge button is clicked, we'll send the // user_name value to the generate_forgot_question() method in the controller. $("#get_challenge").click(function(e) {
// Don't do the normal thing you'd do when clicking a button. e.preventDefault();
// Send a post request to the generate_forgot_question method when the // get_challenge button is clicked. Pass in the JSON "user_name: // user_name_container.val()" (value in the user_name input field to the // generate_forgot_question() method. The callback routine gets a // resultObject(JSON) and a status (not used and not actually // returned). The generate_forgot_question() method will return JSON, // the fourth parameter, to post. $.post( "/generate_forgot_question", {user_name: user_name_container.val()}, function(resultObject, resultStatus) {
var answer_container = $("#challenge_answer");
/* Check if the result contains the correct json and that there is no answer_container already. */ if (answer_container.length == 0) { if (resultObject.challenge_question != undefined) { // Take the resultObject and grab the challenge_question from it and add the // input for the user to submit. var result = [ " Challenge Question: ", resultObject.challenge_question, " ", "", " " ];
// Add the result to the challenge_question at the bottom. $("#challenge_question").append(result.join('')); } else { /* There wasn't a challenge answer returned, so let the user know. */ alert("Could not find user name " + user_name_container.val()); } } }, 'json' ); }); });
I've tried to comment this pretty well, but let's go through it. As always with jQuery we're going to make sure the document is ready before doing anything. Next, we'll grab the user_name container. We're going to use it to get the name the user types in to it and pass it back to the server for processing. Next, we set up a click function for the get_challenge button. We do this so we can use it to send the user name and retrieve the challenge question. Next, we disable the normal thing (submit) that we'd do for a button. Then we set up the post to the generate_forgot_password() method in the main controller. We'll pass the user_name that we get from the user_name_container we grabbed above, set up a function for processing the return value, and finally we'll let everyone know we're passing JSON back as the return value. Now let's look at the processing function. First we check to see if we already have a challenge_answer. If we don't, we check the resultObject and see if we have a challenge_question. If we do, then we put the challenge question and then create an input box for the answer. We then join this new HTML to the end of the challenge_question. There may be (OK probably is) better ways to do this. I'm not really an expert on JavaScript or AJAX or JSON, so if you have suggestions for cleaning this up, please leave some hints in the comments.
<htmlxmlns="http://www.w3.org/1999/xhtml"> <head> <!-- Use the page.css in the public directory and set title based on what's set in the associated method. --> <linkrel="stylesheet"type="text/css"href="/page.css"/>
<!-- Serve jQuery from Google. This appears to be the accepted way of doing things now. --> <scripttype="text/javascript"src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<!-- Need to have the xhtml helper for this next line --> #{ js @page_javascript }
<divid="nav"> <?rifaction.node.to_s=="MainController"?> <!-- Main/User controller --> <!-- Move this next section over to the right side of the screen. It will contain the Login/Register if we're not logged in and the Logout if we are. --> <spanstyle="float: right"> <!-- We're going to use the private method logged_in? here to test if we want to show the Login/Register links or the Logout link --> <?rif !logged_in??> <ahref="#{r(:login)}">Login</a> <ahref="#{r(:register)}">Register</a> <?relse?> <ahref="#{r(:account_settings)}">Account</a> <ahref="#{r(:logout)}">Logout</a> <?rend?> </span>
<!-- These next three will be on the left side and always there --> <ahref="#{r(:index)}">Home</a> | <ahref="#{r(:about)}">About Us</a> | <ahref="#{r(:help)}">Help</a> <?relse?> <!-- Admin controller --> <!-- Move this next section over to the right side of the screen. It will contain the Login/Register if we're not logged in and the Logout if we are. --> <spanstyle="float: right"> <!-- We're going to use the private method logged_in? here to test if we want to show the Login/Register links or the Logout link --> <?rif !logged_in??> <ahref="#{r(:login)}">Login</a> <?relse?> <ahref="#{r(:logout)}">Logout</a> <?rend?> </span>
<!-- These next three will be on the left side and always there --> <ahref="#{r(:main)}">Home</a> <?rend?> </div>
<divid="content"> <!-- Display the actual content. This will come from the method or the associated view/*.xhtml file --> #@content </div>
<!-- Set the footer in the center of the screen. --> <divid="footer"style="text-align: center;"> <h5> Powered by Ramaze </h5> </div> </div> </body> </html>
We've reused this a number of times, so it should look pretty familiar. In the head section, we set up for our JavaScript including for jQuery. This time around, we grab it from Google as per current best practices (possibly the only best practice here). Then we have the JavaScript for our particular page (this being whatever page the user is on that needs JavaScript. Next is the body where we have the "header" (which based on an interesting book I'm reading right now, Transcending CSS by Andy Clark, I'd probably relabel as "branding") with our title. Next is the "nav" section with two parts, one for the admin and one for a normal user. This is followed by the "content" which is really whatever is filled in by each of our methods in the controller and finally we have the footer (once again probably renamed to something like "siteinfo").
Finally, here's our CSS (once again, nothing we haven't seen before).
So, I think that's everything (let me know if I missed anything). This pretty much started out as an example for using mail, but that really proved to be the least interesting part of this given how easy it is to use the Mailit gem.
Let me know if you have any questions or comments and I'll do my best to answer them.