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
<%@ 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.