Redesigned API URL structure. Updated ts_api to implement this.

Implemented ts_api:list_timelines/2.
Adjusted ts_timeline:list/3 to be 0-indexed.
Changed ts_user password hash to use a random salt + SHA1
Added some skeleton testing code.
This commit is contained in:
Jonathan Bernard 2011-02-04 17:19:53 -06:00
parent 1575e25898
commit 6e2e0d5f00
10 changed files with 164 additions and 78 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
db/test/ts_user.DCD Normal file

Binary file not shown.

View File

@ -22,64 +22,90 @@ out(YArg) ->
% ================================== %
%% Entry point to the TimeStamper API dispatch system
dispatch_request(_YArg, []) ->
% no arguments: URL is /ts_api or /ts_api/, show API docs
{page, "/ts_api_doc/index.html"};
dispatch_request(YArg, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
dispatch_request(YArg, [H|T]) ->
Username = path_element_to_atom(H),
case T of
% no additional parameters, show user info
[] -> dispatch_user(YArg, Username);
[Param|Params] ->
Timeline = path_element_to_atom(Param),
dispatch_timeline(YArg, Username, Timeline, Params)
end.
dispatch_user(YArg, Username) ->
Req = YArg#arg.req,
HTTPMethod = Req#http_request.method,
case HTTPMethod of
'GET' -> get_user(YArg, Username);
'PUT' -> put_user(YArg, Username);
'POST' -> put_user(YArg, Username);
'DELETE' -> delete_user(YArg, Username)
end.
% no entries, show timeline pages
dispatch_timeline(YArg, Username, Timeline, []) ->
Req = YArg#arg.req,
HTTPMethod = Req#http_request.method,
case HTTPMethod of
'GET' -> get_timeline(YArg, Username, Timeline);
'PUT' -> put_timeline(YArg, Username, Timeline);
'POST' -> post_timeline(YArg, Username, Timeline);
'DELETE' -> delete_timeline(YArg, Username, Timeline);
_Other -> make_json_405(YArg)
end;
dispatch_timeline(YArg, Username, Timeline, [H|T]) ->
Param = path_element_to_atom(H),
case Param of
list -> list_entries(YArg, Username, Timeline);
by_id -> dispatch_entry_by_id(YArg, Username, Timeline, T);
by_date -> dispatch_entry_by_date(YArg, Username, Timeline, T);
_Other -> make_json_404(YArg)
users -> dispatch_user(YArg, T);
timelines -> dispatch_timeline(YArg, T);
entries -> dispatch_entry(YArg, T);
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
end.
dispatch_entry_by_id(YArg, _Username, _Timeline, []) ->
make_json_404(YArg, [{note, "An entry id is expected in the URL."}]);
dispatch_user(YArg, []) ->
HTTPMethod = (YArg#arg.req)#http_request.method,
dispatch_entry_by_id(YArg, Username, Timeline, [H|_T]) ->
case HTTPMethod of
'PUT' -> put_user(YArg);
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
end;
dispatch_user(YArg, [H|T]) ->
Username = path_element_to_atom(H),
HTTPMethod = (YArg#arg.req)#http_request.method,
case {HTTPMethod, T} of
{'GET', []} -> get_user(YArg, Username);
{'POST', []} -> post_user(YArg, Username);
{'DELETE', []} -> delete_user(YArg, Username);
{_Other, []} -> make_json_405(YArg, [{see_docs, "/ts_api_doc/users"}]);
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/users"}])
end.
% just username, list timelines or create a new one
dispatch_timeline(YArg, [UrlUsername]) ->
Username = path_element_to_atom(UrlUsername),
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
'GET' -> list_timelines(YArg, Username);
'PUT' -> put_timeline(YArg, Username);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
end;
EventId = list_to_integer(H), % TODO: guard against bad input
get_entry_by_id(YArg, Username, Timeline, EventId).
dispatch_timeline(YArg, [UrlUsername, UrlTimelineId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
HTTPMethod = (YArg#arg.req)#http_request.method,
dispatch_entry_by_date(YArg, Username, Timeline, Params) -> todo.
case HTTPMethod of
'GET' -> get_timeline(YArg, Username, TimelineId);
'POST' -> post_timeline(YArg, Username, TimelineId);
'DELETE' -> delete_timeline(YArg, Username, TimelineId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
end;
dispatch_timeline(YArg, _Other) ->
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines"}]).
dispatch_entry(YArg, [UrlUsername, UrlTimelineId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
'GET' -> list_entries(YArg, Username, TimelineId);
'PUT' -> put_entry(YArg, Username, TimelineId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries"}])
end;
dispatch_entry(YArg, [UrlUsername, UrlTimelineId, UrlEntryId]) ->
Username = path_element_to_atom(UrlUsername),
TimelineId = path_element_to_atom(UrlTimelineId),
EntryId = list_to_integer(UrlEntryId), % TODO: catch non-numbers
HTTPMethod = (YArg#arg.req)#http_request.method,
case HTTPMethod of
'GET' -> get_entry(YArg, Username, TimelineId, EntryId);
'POST' -> post_entry(YArg, Username, TimelineId, EntryId);
'DELETE' -> delete_entry(YArg, Username, TimelineId, EntryId);
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/entries"}])
end;
dispatch_entry(YArg, _Other) ->
make_json_404(YArg, [{see_docs, "/ts_api_doc/entries"}]).
% ============================== %
% ======== IMPLEMENTATION ====== %
@ -91,7 +117,7 @@ get_user(YArg, Username) ->
User -> make_json_200(YArg, User)
end.
put_user(YArg, Username) ->
put_user(YArg) ->
% parse the request body
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
@ -125,29 +151,57 @@ post_user(YArg, Username) ->
ok -> make_json_200(YArg, NewRecord);
no_record -> make_json_404(YArg,
[{status, "no_record"}, {see_docs, "/ts_api_doc/user#POST"}]);
[{status, "no_such_user"}, {see_docs, "/ts_api_doc/user#POST"}]);
_Error -> make_json_500(YArg)
end.
delete_user(YArg, Username) -> todo.
list_timelines(YArg, Username) ->
% pull out the POST data
PostData = yaws_api:parse_post(YArg),
% read or default the Start
Start = case lists:keyfind(start, 1, PostData) of
{start, StartVal} -> StartVal; false -> 0 end,
% read or default the Length
Length = case lists:keyfind(length, 1, PostData) of
{length, LengthVal} -> erlang:min(LengthVal, 50);
false -> 50
end,
% list the timelines from the database
Timelines = ts_timeline:list(Username, Start, Length),
% convert them all to their EJSON form
EJSONTimelines = {array, lists:map(fun ts_json:record_to_ejson/1, Timelines)},
% create resposne
JSONResponse = json:encode({struct, [
{status, "ok"},
{timelines, EJSONTimelines}]}),
% return response
{content, "application/json", JSONReponse}.
get_timeline(YArg, Username, TimelineId) ->
% look for timeline
case ts_timeline:lookup(Username, TimelineId) of
% no such timeline, return 404
no_record -> make_json_404(YArg);
% return the timeline data
Timeline -> make_json_200(YArg, Timeline)
end.
put_timeline(YArg, Username, TimelineId) ->
put_timeline(YArg, Username) ->
% parse the request body
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
% parse into a Timeline record
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
% insert into the database
case ts_timeline:new(NewRecord) of
@ -174,8 +228,7 @@ post_timeline(YArg, Username, TimelineId) ->
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
% create the timeline record
EmptyTimeline = #ts_timeline{ref = {Username, TimelineId}},
NewRecord = ts_json:ejson_to_record(EmptyTimeline, EJSON),
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
case ts_timeline:update(NewRecord) of
ok -> make_json_200(YArg, NewRecord);
@ -192,13 +245,19 @@ list_entries(YArg, Username, Timeline) ->
% pull out the POST data
PostData = yaws_api:parse_post(YArg),
% first determine if we are listing by date
case lists:keyfind(byDate, 1, PostData) of
{byDate, "true"} ->
_Other
% read or default the Start
Start = case lists:keyfind(start, 1, PostData) of
{start, StartVal} -> StartVal; false -> 0 end,
% read or default the Length
Length = case lists:keyfind(length, 1, PostData) of
{length, LengthVal} -> LengthVal; false -> 50 end,
{length, LengthVal} -> erlang:min(LengthVal, 500);
false -> 50
end,
% read or default the sort order
SortOrder = case lists:keyfind(order, 1, PostData) of
@ -212,15 +271,13 @@ list_entries(YArg, Username, Timeline) ->
desc -> ts_entry:list_desc({Username, Timeline}, Start, Length)
end.
get_entry_by_id(YArg, Username, Timeline, EventId) ->
case ts_entry:lookup(Username, Timeline, EventId) of
get_entry(YArg, Username, TimelineId, EntryId) -> todo.
% no such record
no_record -> make_json_404(YArg);
put_entry(YArg, Username, TimelineId) -> todo.
% record found
Entry -> make_json_200(YArg, Entry)
end.
post_entry(YArg, Username, TimelineId, EntryId) -> todo.
delete_entry(YArg, Username, TimelineId, EntryId) -> todo.
% ============================== %
% ======== UTIL METHODS ======== %

View File

@ -59,7 +59,6 @@ ejson_to_record(_Empty=#ts_user{}, EJSON) ->
element(2, lists:keyfind(join_date, 1, Fields)))};
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->
% The JSON records do not have username information
{struct, Fields} = EJSON,
Username = element(2, lists:keyfind(username, 1, Fields)),
TimelineId = element(2, lists:keyfind(timeline_id, 1, Fields)),

View File

@ -24,4 +24,4 @@ list(Username, Start, Length) ->
MatchHead = #ts_timeline{ref = {Username, '_'}, _='_'},
mnesia:select(ts_timeline, [{MatchHead, [], ['$_']}])
end),
lists:sublist(Timelines, Start, Length).
lists:sublist(Timelines, Start + 1, Length).

View File

@ -1,5 +1,6 @@
-module(ts_user).
-export([create_table/1, new/1, update/1, lookup/1, list/2]).
%-export([create_table/1, new/1, update/1, lookup/1, list/2]).
-compile(export_all).
-include("ts_db_records.hrl").
-include_lib("stdlib/include/qlc.hrl").
@ -30,17 +31,13 @@ lookup(Username) ->
list(Start, Length) -> ts_common:list(ts_user, Start, Length).
hash_input_record(User=#ts_user{}) ->
% generate the password salt
Salt = generate_salt(),
% hash the password
HashedPwd = hash_pwd(User#ts_user.username, Salt),
% create a new User record
{HashedPwd, Salt} = hash_pwd(User#ts_user.pwd),
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
generate_salt() ->
"This is a worthless salt value only suitable for testing.".
generate_salt() -> crypto:rand_bytes(36).
hash_pwd(Password, Salt) -> do_hash(Password ++ Salt, []).
do_hash([], Hashed) -> Hashed;
do_hash([Char|Pwd], Hashed) -> do_hash(Pwd, [Char + 13 | Hashed]).
hash_pwd(Password) ->
Salt = generate_salt(),
Hashed = crypto:sha(Password ++ Salt),
{Hashed, Salt}.

7
test/ts_test_common.erl Normal file
View File

@ -0,0 +1,7 @@
-module(ts_test_common).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-include_lib("../src/ts_db_records.hrl").

26
test/ts_user_tests.erl Normal file
View File

@ -0,0 +1,26 @@
-module(ts_user_tests).
%-import(ts_test_common, [assertRecordsEqual/2]).
-include_lib("eunit/include/eunit.hrl").
-include_lib("../src/ts_db_records.hrl").
setup() -> ts_test_env:setup().
cleanup(_Status) -> ts_api_env:cleanup(ok).
% ======== TEST DATA ======== %
joe_user() -> #ts_user{username=joeuser, name="Joe User", pwd="ohmy",
email="JoeUser@users.org",
join_date=calendar:now_to_datetime(erlang:now())}.
% ======== TESTS ======== %
new_delete_test_() ->
% test data
JoeUser = joe_user(),
{setup, fun setup/0, fun cleanup/1, {inorder, [
?_test(new(JoeUser)),
?_test(lookup(JoeUser#ts_user.username, JoeUser)),