In this project you will extend your work on Project #4 by adding
forms for logging in, commenting on existing photos, and
uploading new photos. In order to implement these new features
you will need to use the Rails facilities for sessions and validation.
To get started, copy the directory tree for Project #4 to a new
directory named project5
. Do all of your work for this
project in the new directory.
Write a database migration that will add a new attribute login
to the User
model. This attribute is a string containing
the identifier the user will type when logging in (their "login name").
Include code in your migration to initialize the login
attribute
for each existing user to the users's last name, converted to lower case.
Modify the application to support 3 new URLs:
/users/post_login
: the login form posts to this URL.
Its action method checks to ensure that there exists a user with the given
login. If so, it stores the user id in the
session where it can be checked by other code that needs to know
whether a user is logged in. If there is no such user then the
action must redisplay the login form with an appropriate error
message. After a successful login you should redirect to the page
displaying the user's photos.In addition to implementing these URLs, include support for login/logout in the standard layout used for all of your application's pages. If there is no user logged in, the banner at the top of each page should include a small "Login" link to the login page. If there is a user logged in, the banner should include a small message "Hi Alice! Logout", where "Alice" is the first name of the logged-in user and "Logout" is a link to the logout page.
Once you have implemented user login, the next step is to
implement a form for adding comments to existing photos.
Implement a URL /comments/new/id
that displays a form where a user can add a comment for the
photo whose primary key is id.
You should also display the photo on this page so the user
can see it while he/she is typing the comment. The form
should post to the URL /comments/create/id
;
your implementation for this URL should create a new comment
in the database using the Rails models. The comment must
include the identifier of the logged in user and the time when
the comment was created. Make sure that new comments can be
viewed in the same way as pre-existing comments.
Once you've implemented the form for new comments, modify the
page /photos/index/id
to display a
"New Comment" link next to each photo, which will go to the
new-comment form for that photo.
Your implementation must handle the following errors:
Allow users to add new
photos. To do this, implement a URL /photos/new
,
which displays a form where the user can select a photo file
for upload. The form should post to the URL
/photos/create
, which copies the incoming photo
data to a file in the directory
project5/app/assets/images
and creates a new
record in the database containing the name of the
photo file, the creation time, and the identifier of the
user. Also, add a "New Photo" link at an appropriate place
in one of your existing pages, which users can click to
go to the photo upload form.
Your implementation should check to make sure that a user is logged in and prevent photo uploading if not.
Enhance the login mechanism with support for new-user registration and passwords.
Your password mechanism must implement salting, which is a way of storing passwords securely. The salting mechanism is described in the next few paragraphs. One not-very-secure approach would be to store passwords directly in the database. However, if someone is able to read the database (for example, a rogue system administrator) they can easily retrieve all of the passwords for all users.
A better approach is to apply a message digest function such as
SHA-1 to each password, and store only the message digest in the database.
SHA-1 takes a string such as a password as input and produces a 40-character
string of hex digits (called a message digest) as output. The
output has two interesting properties: first, the digest provides a unique
signature for the input string (there is no known way to produce two different
strings with the same digest); second, given a message digest, there is no
known way to produce a string that will generate that digest. When a user
sets their password, you must invoke Digest::SHA1.hexdigest
to generate the SHA-1 digest corresponding
to that password, and store only the digest in the database; once this is
done you can discard the password. When a user enters
a password to login, invoke Digest::SHA1.hexdigest
to compute
the digest, and compare that
digest to what is stored in the database. With this approach, you can make
sure that a user types the correct password when logging in, but if someone
reads the digests from the database they cannot use that information to
log in.
However, the approach of the previous paragraph has one remaining flaw. Suppose an attacker gets a copy of the database containing the digests. Since the SHA-1 function is public, they can employ a fast dictionary attack to guess common passwords. To do this, the attacker takes each word from a dictionary and computes its digest using SHA-1. Then the attacker checks each digest in the database against the digests in the dictionary (this can be done very quickly by putting all of the dictionary digests in a hash table). If any user has chosen a simple dictionary word as their password, the attacker can guess it quickly.
In order to make dictionary attacks more difficult, you must use password
salting. When a user sets their password, compute a random number and
concatenate it with the password before computing the SHA-1 digest (the
rand
method will return a suitable random number). The
random number is called a salt. Then store both the salt and the
digest in the database. When checking passwords during login, retrieve the
salt from the database, concatenate it to the password typed by the user,
and compute the digest of this string for comparison with the digest in
the database. With this approach an attacker who has gained access to the
login database cannot use the simple dictionary attack described above;
the digest of a dictionary word would need to include the salt for a particular
account, which means that the attacker would need to recompute all of the
dictionary digests for every distinct account in the database. This makes
dictionary attacks more expensive.
This salting approach requires one small twist in your code.
The password value that is posted in forms never gets stored in the
database (so it is not specified in migrations, for example). In order to
handle the password correctly, you must define it as a virtual attribute
of the User
model. This simply means that you must define by hand
the two
standard accessor methods password
and password=
in the user model (if you used a migration to create those accessors,
they would automatically read and write from the database record;
your methods will not do that). When password=
is invoked, it
must compute
the salt and the password digest and set the corresponding attributes of
the User
model (which are part of the database).
password_digest
and salt
.password_valid?
to the User
model. This method takes a candidate password as argument and returns
a boolean value indicating whether the password is the correct one for
the user corresponding to this model object./users/new
displays
a form to register a new user, and it posts to
/users/create
. The registration page provides
fields for the new user's first and last names, their login, plus two
fields in which identical copies of the password must be typed. The
post action must make sure that the new login doesn't already exist and that the
two copies of the password are identical. If the information is valid,
then a new user gets created in the database and the action redirects
to the login page. If there is an error then the registration form gets
redisplayed along with appropriate error messages. Be sure to use the
Rails validation mechanism.These points will be awarded if your problem solutions have proper MVC decomposition, follow the Rails conventions, and generate valid XHTML. In addition, your code and templates must be clean and readable, and your Web pages must be at least "reasonably nice" in appearance and convenience.
reset_column_information
method on the model class: this will force Rails to reexamine the database
schema so that it notices any changes to the table structure.form.file_field
to generate the form element.params
hash: if the first argument to
form_for
was :xyz
and the first argument
to form.file_field
was :abc
, then
the uploaded file will be available as
params[:xyz][:abc]
. This is an object of
class ActionController::UploadedFile, which supports IO
methods such as read
. The object also provides
a method original_filename
, which returns the
name of the file that was selected by the user in their
browser.project5/app/assets/images
.DateTime.now
returns
a string containing the
current date and time in the right format for storing in the
database.getlocal
useful.
When applied to a time value, it produces a corresponding time
in the local timezone (by default, times are stored in Greenwich
Mean Time). Note: the best approach is to store times in GMT and
only convert them to local time when displaying for the user.
rake db:migrate:reset
project5/app/assets/images
and
delete all of the new image files you have created, leaving only
the original ones.
Use the standard class submission mechanism
to submit the entire application (everything in the project5
directory). Please indicate in a README file whether you developed
on Windows or a Macintosh (we may need this information in order to
test your solution). Please clean up your project directory before
submitting, as described in the submission instructions.