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:
parent
1575e25898
commit
6e2e0d5f00
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
db/test/ts_user.DCD
Normal file
BIN
db/test/ts_user.DCD
Normal file
Binary file not shown.
187
src/ts_api.erl
187
src/ts_api.erl
@ -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 ======== %
|
||||
|
@ -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)),
|
||||
|
@ -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).
|
||||
|
@ -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
7
test/ts_test_common.erl
Normal 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
26
test/ts_user_tests.erl
Normal 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)),
|
||||
|
Loading…
x
Reference in New Issue
Block a user