Nitrogen is a new Erlang web framework. It is event driven and a very natural fit for Erlang and makes web programming almost enjoyable.
The documentation is much better than when I first started with Nitrogen but there is still some lacking that requires searching through the source code. Here I’ll describe how I implemented users for a current project, still very much in its infancy, I am working on using Nitrogen and Mnesia.
The functions I reference can be found in the source code for the beerenthusiats.org project on github. I will not link directly to the source file for each module but instead the source directory, in case a filename changes or is moved to a subdir in the future. The source for the actual page generation of even the simple pages is long for a blog post so please view those through the github interface. Nitrogen’s templating system and callback system are very simple and you can learn those from Nitrogen’s documentation, thus I don’t bother to explain how the pages work.
We’ll have simple user accounts storing just username, password (hash) and email address. Thus we need a page that allows users to enter this data, look at web_register.erl in the git repo.
This module sends the data to another Erlang module, db_backend, for checking and insertion to the database. The database backend must hash, we’ll use sha1, the password and check that the username is not already in use and finally insert.
First, we need a record that describes the users table:
-record (users, {username, email_address, password}).
Next, we have an init/0 function that is only run once that creates the database and the table.
init () ->
mnesia:create_schema ([node()]),
mnesia:start(),
mnesia:create_table (users, [{attributes, record_info (fields, users)}, {disc_copies, [node()]}]),
mnesia:stop().
The create_table function takes a list of attributes, here we use the record_info function to get that list from the users record, and parameters describing how and where the data is to be stored. Here we use disc_copies and only send node(). This means that the data in users should be stored on disk, but also in main memory, as opposed to storing only on disk or only in memory. Sending node() tells Mnesia we are only storing this table on the current node. Obviously, we can see it would be very easy to store the database across multiple Erlang nodes.
Lastly, we have a start/0 function that is run each time the app is started to start Mnesia and crypto, which is used for hashing the password.
start () -> crypto:start(), mnesia:start().
The add_user/3 (called from web_register) function inserts the record (a table row) to the users table with the write/1 function that you can view in the db_backend module’s source and see how it works and how it uses transactions. You can ignore the call to couchdb_util:db_create/1. However, if you are interested in how I work with Couchdb those files are in the repo as well.
add_user (Username, EmailAddress, Password) ->
<PasswordDigest:160> = crypto:sha(Password),
Row = #users{username=Username, email_address=EmailAddress, password=PasswordDigest},
case write (Row) of
{atomic, Val} ->
couchdb_util:db_create (Username),
ok;
{aborted, Reason} ->
io:format ("Adding user failed!~nRow: ~s aborted.~nReason: ~s~n", [Row, Reason]),
aborted
end.
Now a user is in the database, users must be able to login. web_login is the module containing beerenthusiasts.org basic login page. Here is the main event function from that login page that is called when the user tries to login from that page:
event (login) ->
case db_backend:validate(hd(wf:q(username)), hd(wf:q(pass))) of
{valid, _ID} ->
wf:flash ("Correct"),
wf:user(hd(wf:q(username))),
wf:redirect("your_page");
_ ->
wf:flash ("Incorrect")
end;
This page uses the function validate/2 which takes a username and password from the fields and checks if it matches in the users database:
validate (Username, Password) ->
<PasswordDigest:160> = crypto:sha(Password),
case do (qlc:q ([X#users.username || X <- mnesia:table(users), check (X#users.username,
X#users.email_address, Username), X#users.password == PasswordDigest])) of
fail ->
not_valid;
Results ->
if
length (Results) == 1 ->
{valid, hd(Results)};
true ->
not_valid
end
end.
I won’t go into all the details of how Mnesia is queried, but as you can see from the code above it does not use SQL. Instead, you use list comprehension, the same as anywhere else you use it in Erlang, to query the database tables. qlc:q/1 takes a list comprehension and returns a Mnesia query that can be run, look at the do/1 function in db_backend to see how it is send to Mnesia. List comprehension and the record syntax (X#users.username for example) is a very natural fit for relational algebra and for me personally much nicer than SQL.
Nitrogen provides the function wf:user() to check if a user is currently logged in.
case wf:user() of
undefined ->
wf:redirect("register"),
Header = "";
_ ->
Header = "user_header"
end,
The wf:user/0 function goes to the wf_session module. This module handles creating, destroying and checking sessions. Nitrogen contains a separate process which tracks users and state. This process is queried when a call to a wf function requires knowledge of the current session, if one exists.
Oh my god. Somebody finally went and did it. They wrote a markaby killer for python:
http://nitrogenproject.com/web/samples/viewsource?module=web_samples_simplecontrols
This framework looks amazing. I will be checking out this beerenthusiast project.
s/python/erlang/. Wow what a freudian slip…..
Very nice. Good to see people blogging about Erlang. An indispensable language, and (like Haskell and SBCL) an absolute joy to program in (as opposed to C++/C#/Java/etc).
Happy Hacking.
Hi,
in the validate/2 function, what is this check function called ?
I can’t find it in the github as the project is now on mysql apparently….
(btw, why do you have gone to mysql ?)
Julien (erlang newbie)
Julien, check simply sees if the first argument matches the second or third.
I moved it to MySQL simple for the benefits of MySQL over Mnesia for lots of things. I basically use ETS tables for any data storage I need that isn’t permanent and MySQL for permanent storage.