Thursday, January 11, 2007

ErlyWeb Tutorial: Creating a Simple Login Page

Let's see how to create a simple login component with ErlyWeb.

Update: I simplified the HTML to reduce clutter.

First, let's take a look at login_view.et:


<% Data %>

<%@ index({Username, Errs}) %>
<% [error(Err) || Err <- Errs] %>


login

username:

password:




<%@ error(Err) %>
- <% error_msg(Err) %>


<%@ error_msg({value_too_short, FieldName, Min}) %>The <% FieldName %> must be at least <% Min %> characters.
<%@ error_msg(invalid_credentials) %>Invalid username/password.


As you can see, the 'index' function of login_view.et accepts a 2 element tuple. The first element is the value of the 'username' field, and the second element contains a list of error values. An error value can be any Erlang data type. The error_msg/1 function maps error values to their textual message. This is done by simple pattern-matching (aside: damn, I love pattern-matching :) ).

Before we take a look at the controller, let's implement a nice little helper function that contains some boiler-plate code for form validation.

Update: The following is generic, reusable code that can work for any form validation, not just login pages. It may even be included in the framework at some point.


validate(A, Fields, Fun) ->
Params = yaws_api:parse_post(A),
lists:foldl(
fun(Field, {Vals, Errs}) ->
case proplists:lookup(Field, Params) of
none -> exit({missing_param, Field});
{_, Val} ->
Val1 = case Val of undefined -> ""; _ -> Val end,
Acc1 =
case Fun(Field, Val1) of
ok ->
{[Val1 | Vals], Errs};
Err ->
{[Val1 | Vals], [Err | Errs]}
end,
Acc1
end
end, {[], []}, lists:reverse(Fields)).


This function takes the Yaws arg record, a list of expected field names to validate, and a function object that takes 2 parameters -- the field name and the value -- and returns 'ok' or an error code. The validate/3 function returns a 2 element tuple. The first element is the list of values corresponding to the field names, and the second element is a list of error codes returned by the function object as it was applied to each field. validate/3 also converts empty field values from 'undefined' to empty strings so they can be passed to the view component, which always returns iolists, verbatim.

Now, let's take a look at login_controller.erl:


index(A) ->
case yaws_arg:method(A) of
'GET' ->
{data, {"", []}};
'POST' ->
case validate(A, ["username", "password"], fun validate_field/2) of
{[Username, Password], []} ->
case app_user:find(
{'and',
[{username,'=',Username},
{password,'=',crypto:sha(UserName ++ Password)}]}) of
[] ->
{data, {Username, [invalid_credentials]}};
[User] ->
Key = crypto:rand_bytes(20),
Encoded = http_base_64:encode(binary_to_list(Key)),
app_user:save(app_user:key(User, Key)),
{response,
[yaws_api:setcookie("key", Encoded),
{ewr, main}]}
end;
{[Username, _Password], Errors} ->
{data, {Username, Errors}}
end
end.

validate_field("username", Val) when length(Val) > 4 -> ok;
validate_field("username", _) -> {value_too_short, "username", "5"};
validate_field("password", Val) when length(Val) > 5 -> ok;
validate_field("password", _) -> {value_too_short, "password", "6"} .


If the request is a GET, login_controller passes into the view function an empty string for the username and no errors. The it's a POST, login_controller checks that the fields have passed basic validation, i.e. their values pass the minimum length requirements. If so, it checks that the username/password hash combination is in the database. If is it, login_controller assigns the user a random 20 byte key string as a cookie value, which it also saves in the database (in this example, we use the database for session management, but this isn't required) and then redirects to the 'main' component (note: this 'response' tuple is only available in ErlyWeb 0.4, which hasn't been released yet). If the username/password combination is invalid, login_controller passes to the view function the previous Username value and the 'invalid_credentials' error code.

This example illustrates a few cool things about Erlang/ErlyWeb:

- ErlyDB makes working with the underying RDBMS very simple and natural.
- Pattern matching is a joy, both in validation code and in ErlTL templates.
- ErlSQL lets us create short SQL statement snippets dynamically while avoiding manual iolist construction and guaranteeing protection against SQL injection attacks.
- Everything is so easy! You gotta love Erlang :)

Alright, now back to work.

8 comments:

Charles said...

Please bring the ErlyWeb Google Group back. It's unavailable since... Yesterday... An eternity. Seems I'm getting addicted! Thank you.

evilman said...

I have to say that I love erlang but your web framework code looks really messy. You will not find people who want to code templates like that - people are spoiled and want the code to be clean, that code of yours looks like really early JSPs.

Yariv said...

Charles -- the Group seems to work fine. I didn't realize it was offline.

Evilman -- I've heard many conflicting opinions about ErlTL. Some people love it, others don't. I think that usually once people start using it, they grow to like it. Anyway, if you have specific suggestions for how you think it can be improved, I'll be happy to hear them.

David Marko said...

ErlTL issue ... what about suing something like this:

http://blog.hyperstruct.net/2007/1/7/seethrough-a-simple-xml-xhtml-templating-system-for-erlang

Its based on Python's ZopePageTemplates, which has a good concept.

hyperstruct said...

David,

seethrough is only one month old, and I am nowhere as experienced with Erlang as Yariv is. Please keep both things in mind if you have to choose a templating system. :-)

David Marko said...

Well, ErlyWeb seems to be 3 months old ... :-) Sure its three times more, but still a young baby.

David

khigia said...

What does the code to check that user is logged looks like?

Shall we use the hook function of application or implement before_call in every components? How to pass the user data to all components?

Yariv said...

I think the best approach is to check if the user is logged in the the app controller's hook/1 function and then put the result in the Arg's opaque field before passing the Arg down to other components. I haven't found before_call to be very useful thus far, but it may come in handy for some purposes.