Implemented cookie-based authentication to the API.
Created timestamper module to start the application. Added cookie-based authentication to ts_api. Added utility methods to ts_api: * make_json_400/1 and make_json_400/1 * make_json_401/1 and make_json_401/2 * parse_json_body/1 reads a JSON object from a HTTP request body. Implemented ts_api_session module to manage api user sessions. Fixed ts_entry:list* methods to be 0-indexed. Removed the ts_json:ejson_to_record/1 implementation for ts_user records. Decided that ts_user records are never trusted from the client, manipulation of fields such as pwd, username will be restricted to app pages. Changed the password hashing algorithm. Now uses SHA1(pwd + 256bit salt). Want to use bcrypt, investingating cross-platform bcrypt implementation. Fixed yaws.conf config file.
This commit is contained in:
parent
5809ed3959
commit
0642c18a6e
Binary file not shown.
Binary file not shown.
Binary file not shown.
5
login.post
Normal file
5
login.post
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
POST /ts_api/login HTTP/1.0
|
||||||
|
Content-Type: application/json
|
||||||
|
Content-Length: 45
|
||||||
|
|
||||||
|
{"username":"jdbernard", "password":"Y0uthc"}
|
16
src/timestamper.erl
Normal file
16
src/timestamper.erl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
-module(timestamper).
|
||||||
|
-export([start/0, create_tables/1]).
|
||||||
|
|
||||||
|
start() ->
|
||||||
|
ok = application:load(mnesia),
|
||||||
|
ok = application:set_env(mnesia, dir, "/home/jdbernard/projects/timestamper/web-app/db/test"),
|
||||||
|
ok = mnesia:start(),
|
||||||
|
ok.
|
||||||
|
|
||||||
|
create_tables(Nodes) ->
|
||||||
|
TableOpts = [{disc_copies, Nodes}],
|
||||||
|
{atomic, ok} = id_counter:create_table(TableOpts),
|
||||||
|
{atomic, ok} = ts_user:create_table(TableOpts),
|
||||||
|
{atomic, ok} = ts_timeline:create_table(TableOpts),
|
||||||
|
{atmoic, ok} = ts_entry:create_table(TableOpts),
|
||||||
|
ok.
|
277
src/ts_api.erl
277
src/ts_api.erl
@ -5,6 +5,9 @@
|
|||||||
-include("yaws_api.hrl").
|
-include("yaws_api.hrl").
|
||||||
|
|
||||||
out(YArg) ->
|
out(YArg) ->
|
||||||
|
% retreive the session data
|
||||||
|
Session = ts_api_session:get_session(YArg),
|
||||||
|
|
||||||
%get the app mod data
|
%get the app mod data
|
||||||
PathString = YArg#arg.appmoddata,
|
PathString = YArg#arg.appmoddata,
|
||||||
|
|
||||||
@ -14,54 +17,74 @@ out(YArg) ->
|
|||||||
_Any -> re:split(PathString, "/", [{return, list}])
|
_Any -> re:split(PathString, "/", [{return, list}])
|
||||||
end,
|
end,
|
||||||
|
|
||||||
% process request
|
% process the request
|
||||||
dispatch_request(YArg, PathElements).
|
case catch dispatch_request(YArg, Session, PathElements) of
|
||||||
|
{'EXIT', Err} ->
|
||||||
|
% TODO: log error internally
|
||||||
|
io:format("~p", [Err]),
|
||||||
|
make_json_500(YArg);
|
||||||
|
Other -> Other
|
||||||
|
end.
|
||||||
|
|
||||||
% ================================== %
|
% ================================== %
|
||||||
% ======== DISPATCH METHODS ======== %
|
% ======== DISPATCH METHODS ======== %
|
||||||
% ================================== %
|
% ================================== %
|
||||||
|
|
||||||
%% Entry point to the TimeStamper API dispatch system
|
%% Entry point to the TimeStamper API dispatch system
|
||||||
dispatch_request(YArg, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
|
dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
|
||||||
|
|
||||||
dispatch_request(YArg, [H|T]) ->
|
dispatch_request(YArg, Session, [H|T]) ->
|
||||||
Param = path_element_to_atom(H),
|
Param = path_element_to_atom(H),
|
||||||
|
|
||||||
case Param of
|
case {Session, Param} of
|
||||||
users -> dispatch_user(YArg, T);
|
{_, login} -> do_login(YArg);
|
||||||
timelines -> dispatch_timeline(YArg, T);
|
{_, logout} -> do_logout(YArg);
|
||||||
entries -> dispatch_entry(YArg, T);
|
|
||||||
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
|
{not_logged_in, _} -> make_json_401(YArg);
|
||||||
|
{session_expired, _} -> make_json_401(YArg, [{status, "session expired"}]);
|
||||||
|
|
||||||
|
{_S, users} -> dispatch_user(YArg, Session, T);
|
||||||
|
{_S, timelines} -> dispatch_timeline(YArg, Session, T);
|
||||||
|
{_S, entries} -> dispatch_entry(YArg, Session, T);
|
||||||
|
{_S, _Other} -> make_json_404(YArg, [{see_docs, "/ts_api_doc/"}])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
dispatch_user(YArg, []) ->
|
dispatch_user(YArg, _Session, []) ->
|
||||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/"}]);
|
||||||
|
|
||||||
case HTTPMethod of
|
dispatch_user(YArg, Session, [H]) ->
|
||||||
'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),
|
Username = path_element_to_atom(H),
|
||||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
case {HTTPMethod, T} of
|
% compare to the logged-in user
|
||||||
{'GET', []} -> get_user(YArg, Username);
|
case {HTTPMethod, Session#ts_api_session.username} of
|
||||||
{'POST', []} -> post_user(YArg, Username);
|
|
||||||
{'DELETE', []} -> delete_user(YArg, Username);
|
{'GET', Username} -> get_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"}])
|
{_BadMethod, Username} ->
|
||||||
|
make_json_405(YArg, [{see_docs, "/ts_api_doc/users"}]);
|
||||||
|
|
||||||
|
_Other -> make_json_401(YArg, [{see_docs, "/ts_api_doc/users"}])
|
||||||
end.
|
end.
|
||||||
|
|
||||||
% just username, list timelines or create a new one
|
dispatch_timeline(YArg, _Session, []) ->
|
||||||
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines"}]);
|
||||||
|
|
||||||
|
dispatch_timeline(YArg, Session, [UrlUsername|_T] = PathElements) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
|
||||||
|
case Session#ts_api_session.username of
|
||||||
|
Username -> dispatch_timeline(YArg, PathElements);
|
||||||
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/users"}])
|
||||||
|
end.
|
||||||
|
|
||||||
|
% just username, list timelines
|
||||||
dispatch_timeline(YArg, [UrlUsername]) ->
|
dispatch_timeline(YArg, [UrlUsername]) ->
|
||||||
Username = path_element_to_atom(UrlUsername),
|
Username = path_element_to_atom(UrlUsername),
|
||||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
case HTTPMethod of
|
case HTTPMethod of
|
||||||
'GET' -> list_timelines(YArg, Username);
|
'GET' -> list_timelines(YArg, Username);
|
||||||
'PUT' -> put_timeline(YArg, Username);
|
|
||||||
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@ -71,15 +94,27 @@ dispatch_timeline(YArg, [UrlUsername, UrlTimelineId]) ->
|
|||||||
HTTPMethod = (YArg#arg.req)#http_request.method,
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
||||||
|
|
||||||
case HTTPMethod of
|
case HTTPMethod of
|
||||||
'GET' -> get_timeline(YArg, Username, TimelineId);
|
'GET' -> get_timeline(YArg, Username, TimelineId);
|
||||||
'POST' -> post_timeline(YArg, Username, TimelineId);
|
'POST' -> post_timeline(YArg, Username, TimelineId);
|
||||||
'DELETE' -> delete_timeline(YArg, Username, TimelineId);
|
'PUT' -> put_timeline(YArg, Username, TimelineId);
|
||||||
|
'DELETE' -> delete_timeline(YArg, Username, TimelineId);
|
||||||
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines"}])
|
||||||
end;
|
end;
|
||||||
|
|
||||||
dispatch_timeline(YArg, _Other) ->
|
dispatch_timeline(YArg, _Other) ->
|
||||||
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines"}]).
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines"}]).
|
||||||
|
|
||||||
|
dispatch_entry(YArg, _Session, []) ->
|
||||||
|
make_json_404(YArg, [{see_docs, "/ts_aip_doc/entries"}]);
|
||||||
|
|
||||||
|
dispatch_entry(YArg, Session, [UrlUsername|_T] = PathElements) ->
|
||||||
|
Username = path_element_to_atom(UrlUsername),
|
||||||
|
|
||||||
|
case Session#ts_api_session.username of
|
||||||
|
Username -> dispatch_entry(YArg, PathElements);
|
||||||
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_doc/entries"}])
|
||||||
|
end.
|
||||||
|
|
||||||
dispatch_entry(YArg, [UrlUsername, UrlTimelineId]) ->
|
dispatch_entry(YArg, [UrlUsername, UrlTimelineId]) ->
|
||||||
Username = path_element_to_atom(UrlUsername),
|
Username = path_element_to_atom(UrlUsername),
|
||||||
TimelineId = path_element_to_atom(UrlTimelineId),
|
TimelineId = path_element_to_atom(UrlTimelineId),
|
||||||
@ -111,53 +146,49 @@ dispatch_entry(YArg, _Other) ->
|
|||||||
% ======== IMPLEMENTATION ====== %
|
% ======== IMPLEMENTATION ====== %
|
||||||
% ============================== %
|
% ============================== %
|
||||||
|
|
||||||
|
do_login(YArg) ->
|
||||||
|
EJSON = parse_json_body(YArg),
|
||||||
|
|
||||||
|
{struct, Fields} = EJSON,
|
||||||
|
|
||||||
|
case {lists:keyfind(username, 1, Fields),
|
||||||
|
lists:keyfind(password, 1, Fields)} of
|
||||||
|
|
||||||
|
% username an password found
|
||||||
|
{{username, UnameField}, {password, Password}} ->
|
||||||
|
Username = list_to_atom(UnameField),
|
||||||
|
|
||||||
|
% check the uname, password
|
||||||
|
case ts_user:check_credentials(Username, Password) of
|
||||||
|
% they are good
|
||||||
|
true ->
|
||||||
|
{CookieVal, _Session} = ts_api_session:new(Username),
|
||||||
|
|
||||||
|
[{content, "application/json",
|
||||||
|
json:encode({struct, [{status, "ok"}]})},
|
||||||
|
{header, {set_cookie, io_lib:format(
|
||||||
|
"ts_api_session=~s; Path=/ts_api; httponly",
|
||||||
|
[CookieVal])}}];
|
||||||
|
|
||||||
|
% they are not good
|
||||||
|
false -> make_json_401(YArg, [{status,
|
||||||
|
"bad username/password combination"}])
|
||||||
|
end;
|
||||||
|
|
||||||
|
_Other -> make_json_400(YArg, [{see_docs, "/ts_api_doc/login"}])
|
||||||
|
end.
|
||||||
|
|
||||||
|
do_logout(YArg) ->
|
||||||
|
Cookie = (YArg#arg.headers)#headers.cookie,
|
||||||
|
CookieVal = yaws_api:find_cookie_val("ts_api_session", Cookie),
|
||||||
|
ts_api_session:logout(CookieVal).
|
||||||
|
|
||||||
get_user(YArg, Username) ->
|
get_user(YArg, Username) ->
|
||||||
case ts_user:lookup(Username) of
|
case ts_user:lookup(Username) of
|
||||||
no_record -> make_json_404(YArg);
|
no_record -> make_json_404(YArg);
|
||||||
User -> make_json_200(YArg, User)
|
User -> make_json_200(YArg, User)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
put_user(YArg) ->
|
|
||||||
% parse the request body
|
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
|
||||||
|
|
||||||
% parse into a user record
|
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_user{}, EJSON),
|
|
||||||
|
|
||||||
case ts_user:new(NewRecord) of
|
|
||||||
% record created
|
|
||||||
ok -> [{status, 201}, make_json_200(YArg, NewRecord)];
|
|
||||||
|
|
||||||
% will not create, record exists
|
|
||||||
{error, {record_exists, ExistingRecord}} ->
|
|
||||||
JSONResponse = json:encode({struct, [
|
|
||||||
{status, "username taken"},
|
|
||||||
{see_docs, "/ts_api_doc/user#PUT"}
|
|
||||||
]}),
|
|
||||||
|
|
||||||
[{status, 409}, {content, "application/json", JSONResponse}];
|
|
||||||
|
|
||||||
_Error -> make_json_500(YArg)
|
|
||||||
end.
|
|
||||||
|
|
||||||
post_user(YArg, Username) ->
|
|
||||||
% parse the POST data
|
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
|
||||||
|
|
||||||
% create the user record
|
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_user{}, EJSON),
|
|
||||||
|
|
||||||
case ts_user:update(NewRecord) of
|
|
||||||
ok -> make_json_200(YArg, NewRecord);
|
|
||||||
|
|
||||||
no_record -> make_json_404(YArg,
|
|
||||||
[{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) ->
|
list_timelines(YArg, Username) ->
|
||||||
% pull out the POST data
|
% pull out the POST data
|
||||||
PostData = yaws_api:parse_post(YArg),
|
PostData = yaws_api:parse_post(YArg),
|
||||||
@ -187,24 +218,27 @@ list_timelines(YArg, Username) ->
|
|||||||
{timelines, EJSONTimelines}]}),
|
{timelines, EJSONTimelines}]}),
|
||||||
|
|
||||||
% return response
|
% return response
|
||||||
{content, "application/json", JSONReponse}.
|
{content, "application/json", JSONResponse}.
|
||||||
|
|
||||||
get_timeline(YArg, Username, TimelineId) ->
|
get_timeline(YArg, Username, TimelineId) ->
|
||||||
% look for timeline
|
% look for timeline
|
||||||
case ts_timeline:lookup(Username, TimelineId) of
|
case ts_timeline:lookup(Username, TimelineId) of
|
||||||
% no such timeline, return 404
|
% no such timeline, return 404
|
||||||
no_record -> make_json_404(YArg, [{status, "no_such_timeline"}]);
|
no_record -> make_json_404(YArg, [{status, "no such timeline"}]);
|
||||||
% return the timeline data
|
% return the timeline data
|
||||||
Timeline -> make_json_200(YArg, Timeline)
|
Timeline -> make_json_200(YArg, Timeline)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
put_timeline(YArg, Username) ->
|
put_timeline(YArg, Username, TimelineId) ->
|
||||||
|
|
||||||
% parse the request body
|
% parse the request body
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
EJSON = parse_json_body(YArg),
|
||||||
|
|
||||||
% parse into a Timeline record
|
% parse into a Timeline record
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
TR = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
||||||
|
|
||||||
|
% set username and timeline id
|
||||||
|
NewRecord = TR#ts_timeline{ref = {Username, TimelineId}},
|
||||||
|
|
||||||
% insert into the database
|
% insert into the database
|
||||||
case ts_timeline:new(NewRecord) of
|
case ts_timeline:new(NewRecord) of
|
||||||
@ -228,22 +262,32 @@ put_timeline(YArg, Username) ->
|
|||||||
|
|
||||||
post_timeline(YArg, Username, TimelineId) ->
|
post_timeline(YArg, Username, TimelineId) ->
|
||||||
% parse the POST data
|
% parse the POST data
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
EJSON = parse_json_body(YArg),
|
||||||
|
%{struct, Fields} = EJSON,
|
||||||
|
|
||||||
% create the timeline record
|
% parse into a timeline record
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
TR = ts_json:ejson_to_record(#ts_timeline{}, EJSON),
|
||||||
|
|
||||||
|
% not supported right now, would require changing all the entry keys
|
||||||
|
% check to see if they are changing the timeline id
|
||||||
|
%NewTimelineId = case lists:keyfind(1, timeline_id, Fields) of
|
||||||
|
% {timeline_id, Field} -> list_to_atom(Field);
|
||||||
|
% false -> TimelineId end,
|
||||||
|
|
||||||
|
% set username and timeline id
|
||||||
|
NewRecord = TR#ts_timeline{ref = {Username, TimelineId}},
|
||||||
|
|
||||||
case ts_timeline:update(NewRecord) of
|
case ts_timeline:update(NewRecord) of
|
||||||
ok -> make_json_200(YArg, NewRecord);
|
ok -> make_json_200(YArg, NewRecord);
|
||||||
|
|
||||||
no_record -> make_json_404(YArg,
|
no_record -> make_json_404(YArg,
|
||||||
[{status, "no_such_timeline"},
|
[{status, "no such timeline"},
|
||||||
{see_docs, "/ts_api_doc/timelines#POST"}]);
|
{see_docs, "/ts_api_doc/timelines#POST"}]);
|
||||||
|
|
||||||
_Error -> make_json_500(YArg)
|
_Error -> make_json_500(YArg)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete_timeline(YArg, Username, TimelineId) -> {status, 405}.
|
delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}.
|
||||||
|
|
||||||
list_entries(YArg, Username, TimelineId) ->
|
list_entries(YArg, Username, TimelineId) ->
|
||||||
% pull out the POST data
|
% pull out the POST data
|
||||||
@ -254,7 +298,7 @@ list_entries(YArg, Username, TimelineId) ->
|
|||||||
lists:keyfind(byDate, 1, PostData)} of
|
lists:keyfind(byDate, 1, PostData)} of
|
||||||
|
|
||||||
{no_record, _ByDateField} -> make_json_404(
|
{no_record, _ByDateField} -> make_json_404(
|
||||||
[{status, "no_such_timeline"},
|
[{status, "no such timeline"},
|
||||||
{see_docs, "/ts_api_doc/entries#LIST"}]);
|
{see_docs, "/ts_api_doc/entries#LIST"}]);
|
||||||
|
|
||||||
% listing by date range
|
% listing by date range
|
||||||
@ -275,7 +319,7 @@ list_entries(YArg, Username, TimelineId) ->
|
|||||||
end,
|
end,
|
||||||
|
|
||||||
% read sort order and list entries
|
% read sort order and list entries
|
||||||
Entries = case lists:keyfind(order, 1, PostData) ->
|
Entries = case lists:keyfind(order, 1, PostData) of
|
||||||
% descending sort order
|
% descending sort order
|
||||||
{order, "desc"} -> ts_entry:list_desc(
|
{order, "desc"} -> ts_entry:list_desc(
|
||||||
{Username, TimelineId}, StartDate, EndDate);
|
{Username, TimelineId}, StartDate, EndDate);
|
||||||
@ -292,7 +336,7 @@ list_entries(YArg, Username, TimelineId) ->
|
|||||||
{status, "ok"},
|
{status, "ok"},
|
||||||
{entries, EJSONEntries}]}),
|
{entries, EJSONEntries}]}),
|
||||||
|
|
||||||
{content, "application/json", JSONResponse;
|
{content, "application/json", JSONResponse};
|
||||||
|
|
||||||
% listing by table position
|
% listing by table position
|
||||||
_Other ->
|
_Other ->
|
||||||
@ -326,13 +370,13 @@ list_entries(YArg, Username, TimelineId) ->
|
|||||||
{status, "ok"},
|
{status, "ok"},
|
||||||
{entries, EJSONEntries}]}),
|
{entries, EJSONEntries}]}),
|
||||||
|
|
||||||
{content, "application/json", JSONResponse
|
{content, "application/json", JSONResponse}
|
||||||
end.
|
end.
|
||||||
|
|
||||||
get_entry(YArg, Username, TimelineId, EntryId) ->
|
get_entry(YArg, Username, TimelineId, EntryId) ->
|
||||||
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
||||||
% no such entry
|
% no such entry
|
||||||
no_record -> make_json_404(YArg, [{status, "no_such_entry"}]);
|
no_record -> make_json_404(YArg, [{status, "no such entry"}]);
|
||||||
% return the entry data
|
% return the entry data
|
||||||
Entry -> make_json_200(YArg, Entry)
|
Entry -> make_json_200(YArg, Entry)
|
||||||
end.
|
end.
|
||||||
@ -340,14 +384,13 @@ get_entry(YArg, Username, TimelineId, EntryId) ->
|
|||||||
put_entry(YArg, Username, TimelineId) ->
|
put_entry(YArg, Username, TimelineId) ->
|
||||||
|
|
||||||
% parse the request body
|
% parse the request body
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
EJSON = parse_json_body(YArg),
|
||||||
|
|
||||||
% pull out the entry data
|
|
||||||
{struct, Fields} = EJSON,
|
|
||||||
EJSONEntry = lists:findkey(entry, 1, Fields), % TODO: check for errors
|
|
||||||
|
|
||||||
% parse into ts_entry record
|
% parse into ts_entry record
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_entry{}, EJSONEntry),
|
ER = ts_json:ejson_to_record(#ts_entry{}, EJSON),
|
||||||
|
|
||||||
|
% set username and timeline id
|
||||||
|
NewRecord = ER#ts_entry{ref = {Username, TimelineId, undef}},
|
||||||
|
|
||||||
case ts_entry:new(NewRecord) of
|
case ts_entry:new(NewRecord) of
|
||||||
% record created
|
% record created
|
||||||
@ -362,7 +405,7 @@ put_entry(YArg, Username, TimelineId) ->
|
|||||||
{see_docs, "/ts_api_doc/entries#PUT"}
|
{see_docs, "/ts_api_doc/entries#PUT"}
|
||||||
]}),
|
]}),
|
||||||
|
|
||||||
{content, "application/json", JSONResposne};
|
{content, "application/json", JSONResponse};
|
||||||
|
|
||||||
_Error -> make_json_500(YArg)
|
_Error -> make_json_500(YArg)
|
||||||
end.
|
end.
|
||||||
@ -370,25 +413,24 @@ put_entry(YArg, Username, TimelineId) ->
|
|||||||
post_entry(YArg, Username, TimelineId, EntryId) ->
|
post_entry(YArg, Username, TimelineId, EntryId) ->
|
||||||
|
|
||||||
% parse the POST data
|
% parse the POST data
|
||||||
{done, {ok, EJSON}, _} = json:decode([], binary_to_list(YArg#arg.clidata)),
|
EJSON = parse_json_body(YArg),
|
||||||
|
|
||||||
% pull out the entry data
|
|
||||||
{struct, Fields} = EJSON,
|
|
||||||
EJSONEntry = lists:findkey(entry, 1, Fields), % TODO: error handling
|
|
||||||
|
|
||||||
% parse into ts_entry record
|
% parse into ts_entry record
|
||||||
NewRecord = ts_json:ejson_to_record(#ts_entry{}, EJSONEntry),
|
ER = ts_json:ejson_to_record(#ts_entry{}, EJSON),
|
||||||
|
|
||||||
|
% set uername, timeline id, and entry id
|
||||||
|
NewRecord = ER#ts_entry{ref = {Username, TimelineId, EntryId}},
|
||||||
|
|
||||||
case ts_entry:update(NewRecord) of
|
case ts_entry:update(NewRecord) of
|
||||||
ok -> make_json_200(YArg, NewRecord);
|
ok -> make_json_200(YArg, NewRecord);
|
||||||
|
|
||||||
no_record -> make_json_404(YArg,
|
no_record -> make_json_404(YArg,
|
||||||
[{status, "no_such_entry"}, {see_docs, "/ts_api_doc/entries#POST"}]);
|
[{status, "no such entry"}, {see_docs, "/ts_api_doc/entries#POST"}]);
|
||||||
|
|
||||||
_Error -> make_json_500(YArg)
|
_Error -> make_json_500(YArg)
|
||||||
end.
|
end.
|
||||||
|
|
||||||
delete_entry(YArg, Username, TimelineId, EntryId) -> todo.
|
delete_entry(_YArg, _Username, _TimelineId, _EntryId) -> todo.
|
||||||
|
|
||||||
% ============================== %
|
% ============================== %
|
||||||
% ======== UTIL METHODS ======== %
|
% ======== UTIL METHODS ======== %
|
||||||
@ -398,12 +440,21 @@ delete_entry(YArg, Username, TimelineId, EntryId) -> todo.
|
|||||||
path_element_to_atom(PE) ->
|
path_element_to_atom(PE) ->
|
||||||
list_to_atom(re:replace(PE, "\\s", "_", [{return, list}])).
|
list_to_atom(re:replace(PE, "\\s", "_", [{return, list}])).
|
||||||
|
|
||||||
|
parse_json_body(YArg) ->
|
||||||
|
case catch json:decode([], binary_to_list(YArg#arg.clidata)) of
|
||||||
|
{done, {ok, EJSON}, _} -> EJSON;
|
||||||
|
Error ->
|
||||||
|
% TODO: log error internally
|
||||||
|
io:format("~p", [Error]),
|
||||||
|
throw(make_json_400(YArg))
|
||||||
|
end.
|
||||||
|
|
||||||
%% Create a JSON 200 response.
|
%% Create a JSON 200 response.
|
||||||
make_json_200(YArg, Record) ->
|
make_json_200(_YArg, Record) ->
|
||||||
EJSONRecord = ts_json:record_to_ejson(Record),
|
EJSONRecord = ts_json:record_to_ejson(Record),
|
||||||
Tag = case element(1, Record) of
|
Tag = case element(1, Record) of
|
||||||
ts_user -> user;
|
ts_user -> user;
|
||||||
ts_timeilne -> timeline;
|
ts_timeline -> timeline;
|
||||||
ts_entry -> entry
|
ts_entry -> entry
|
||||||
end,
|
end,
|
||||||
JSONResponse = json:encode({struct, [
|
JSONResponse = json:encode({struct, [
|
||||||
@ -413,12 +464,31 @@ make_json_200(YArg, Record) ->
|
|||||||
|
|
||||||
{content, "application/json", JSONResponse}.
|
{content, "application/json", JSONResponse}.
|
||||||
|
|
||||||
|
make_json_400(YArg) -> make_json_400(YArg, []).
|
||||||
|
make_json_400(_YArg, Fields) ->
|
||||||
|
F1 = case lists:keyfind(status, 1, Fields) of
|
||||||
|
false -> Fields ++ [{status, "bad request"}];
|
||||||
|
_Else -> Fields
|
||||||
|
end,
|
||||||
|
|
||||||
|
[{status, 400}, {content, "application/json", json:encode({struct, F1})}].
|
||||||
|
|
||||||
|
make_json_401(YArg) -> make_json_401(YArg, []).
|
||||||
|
make_json_401(_YArg, Fields) ->
|
||||||
|
% add default status if not provided
|
||||||
|
F1 = case lists:keyfind(status, 1, Fields) of
|
||||||
|
false -> Fields ++ [{status, "unauthorized"}];
|
||||||
|
_Else -> Fields
|
||||||
|
end,
|
||||||
|
|
||||||
|
[{status, 401}, {content, "application/json", json:encode({struct, F1})}].
|
||||||
|
|
||||||
%% Create a JSON 404 response.
|
%% Create a JSON 404 response.
|
||||||
make_json_404(YArg) -> make_json_404(YArg, []).
|
make_json_404(YArg) -> make_json_404(YArg, []).
|
||||||
make_json_404(YArg, Fields) ->
|
make_json_404(YArg, Fields) ->
|
||||||
% add default status if not provided
|
% add default status if not provided
|
||||||
F1 = case lists:keyfind(status, 1, Fields) of
|
F1 = case lists:keyfind(status, 1, Fields) of
|
||||||
false -> Fields ++ [{status, "not_found"}];
|
false -> Fields ++ [{status, "not found"}];
|
||||||
_Else -> Fields
|
_Else -> Fields
|
||||||
end,
|
end,
|
||||||
|
|
||||||
@ -442,6 +512,5 @@ make_json_405(YArg, Fields) ->
|
|||||||
|
|
||||||
make_json_500(_YArg) ->
|
make_json_500(_YArg) ->
|
||||||
EJSON = {struct, [
|
EJSON = {struct, [
|
||||||
{status, "error"},
|
{status, "internal server error"}]},
|
||||||
{error, "Internal Server Error"}]},
|
|
||||||
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
|
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
|
||||||
|
58
src/ts_api_session.erl
Normal file
58
src/ts_api_session.erl
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
-module(ts_api_session).
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
-include("ts_db_records.hrl").
|
||||||
|
-include("yaws_api.hrl").
|
||||||
|
|
||||||
|
new(Username) ->
|
||||||
|
DateTime = calendar:now_to_universal_time(erlang:now()),
|
||||||
|
Seconds = calendar:datetime_to_gregorian_seconds(DateTime),
|
||||||
|
|
||||||
|
Session = #ts_api_session{
|
||||||
|
username = Username,
|
||||||
|
expires = Seconds + 600 % timeout is 10 minutes
|
||||||
|
},
|
||||||
|
|
||||||
|
CookieVal = yaws_api:new_cookie_session(Session),
|
||||||
|
{CookieVal, Session}.
|
||||||
|
|
||||||
|
logout(CookieVal) ->
|
||||||
|
yaws_api:delete_cookie_session(CookieVal).
|
||||||
|
|
||||||
|
get_session(YArg) ->
|
||||||
|
% get the cookie header
|
||||||
|
Cookie = (YArg#arg.headers)#headers.cookie,
|
||||||
|
|
||||||
|
% get the current server time
|
||||||
|
Now = calendar:now_to_universal_time(erlang:now()),
|
||||||
|
NowSeconds = calendar:datetime_to_gregorian_seconds(Now),
|
||||||
|
|
||||||
|
% look up the cookie in the session server
|
||||||
|
case yaws_api:find_cookie_val("ts_api_session", Cookie) of
|
||||||
|
% no cookie, not logged in
|
||||||
|
[] -> not_logged_in;
|
||||||
|
|
||||||
|
% found the cookie
|
||||||
|
CookieVal ->
|
||||||
|
% get the session data
|
||||||
|
case yaws_api:cookieval_to_opaque(CookieVal) of
|
||||||
|
{error, _} -> not_logged_in;
|
||||||
|
|
||||||
|
{ok, Session} ->
|
||||||
|
if
|
||||||
|
% if the cookie has expired
|
||||||
|
NowSeconds > Session#ts_api_session.expires ->
|
||||||
|
logout(CookieVal),
|
||||||
|
session_expired;
|
||||||
|
|
||||||
|
% cookie is fresh
|
||||||
|
true ->
|
||||||
|
% update the expiry time
|
||||||
|
NewSession = Session#ts_api_session{expires = NowSeconds + 500},
|
||||||
|
yaws_api:replace_cookie_session(CookieVal, NewSession),
|
||||||
|
|
||||||
|
% return cookie
|
||||||
|
NewSession
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end.
|
@ -19,3 +19,13 @@
|
|||||||
mark, % String description of entry
|
mark, % String description of entry
|
||||||
notes % String with further notes about the entry
|
notes % String with further notes about the entry
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-record(ts_api_session, {
|
||||||
|
username,
|
||||||
|
expires
|
||||||
|
}).
|
||||||
|
|
||||||
|
%-record(ts_session, {
|
||||||
|
%session_id,
|
||||||
|
%expires,
|
||||||
|
%username
|
||||||
|
@ -50,7 +50,7 @@ when is_integer(Start) and is_integer(Length) ->
|
|||||||
|
|
||||||
% return only the range selected.
|
% return only the range selected.
|
||||||
% TODO: can we do this without selecting all entries?
|
% TODO: can we do this without selecting all entries?
|
||||||
lists:sublist(SortedEntries, Start, Length);
|
lists:sublist(SortedEntries, Start + 1, Length);
|
||||||
|
|
||||||
list({Username, Timeline}, StartDateTime, EndDateTime, OrderFun) ->
|
list({Username, Timeline}, StartDateTime, EndDateTime, OrderFun) ->
|
||||||
|
|
||||||
|
@ -43,39 +43,19 @@ encode_datetime({{Year, Month, Day}, {Hour, Minute, Second}}) ->
|
|||||||
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
|
lists:flatten(io_lib:format("~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ",
|
||||||
[Year, Month, Day, Hour, Minute, Second])).
|
[Year, Month, Day, Hour, Minute, Second])).
|
||||||
|
|
||||||
ejson_to_record(_Empty=#ts_user{}, EJSON) ->
|
|
||||||
{struct, Fields} = EJSON,
|
|
||||||
|
|
||||||
Pwd = case lists:keyfind(password, 1, Fields) of
|
|
||||||
false -> uninit; Field -> element(2, Field) end,
|
|
||||||
|
|
||||||
#ts_user{
|
|
||||||
username = element(2, lists:keyfind(username, 1, Fields)),
|
|
||||||
pwd = Pwd,
|
|
||||||
pwd_salt = uninit,
|
|
||||||
name = element(2, lists:keyfind(name, 1, Fields)),
|
|
||||||
email = element(2, lists:keyfind(email, 1, Fields)),
|
|
||||||
join_date = decode_datetime(
|
|
||||||
element(2, lists:keyfind(join_date, 1, Fields)))};
|
|
||||||
|
|
||||||
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->
|
ejson_to_record(_Empty=#ts_timeline{}, EJSON) ->
|
||||||
{struct, Fields} = EJSON,
|
{struct, Fields} = EJSON,
|
||||||
Username = element(2, lists:keyfind(username, 1, Fields)),
|
|
||||||
TimelineId = element(2, lists:keyfind(timeline_id, 1, Fields)),
|
|
||||||
|
|
||||||
#ts_timeline{
|
#ts_timeline{
|
||||||
ref = {Username, TimelineId},
|
ref = {undef, undef},
|
||||||
created = decode_datetime(element(2, lists:keyfind(created, 1, Fields))),
|
created = decode_datetime(element(2, lists:keyfind(created, 1, Fields))),
|
||||||
desc = element(2, lists:keyfind(description, 1, Fields))};
|
desc = element(2, lists:keyfind(description, 1, Fields))};
|
||||||
|
|
||||||
ejson_to_record(_Empty=#ts_entry{}, EJSON) ->
|
ejson_to_record(_Empty=#ts_entry{}, EJSON) ->
|
||||||
{struct, Fields} = EJSON,
|
{struct, Fields} = EJSON,
|
||||||
Username = element(2, lists:keyfind(username, 1, Fields)),
|
|
||||||
TimelineId = element(2, lists:keyfind(timeline_id, 1, Fields)),
|
|
||||||
EntryId = element(2, lists:keyfind(entry_id, 1, Fields)),
|
|
||||||
|
|
||||||
#ts_entry{
|
#ts_entry{
|
||||||
ref = {Username, TimelineId, EntryId},
|
ref = {undef, undef, undef},
|
||||||
timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime(
|
timestamp = calendar:datetime_to_gregorian_seconds(decode_datetime(
|
||||||
element(2, lists:keyfind(timestamp, 1, Fields)))),
|
element(2, lists:keyfind(timestamp, 1, Fields)))),
|
||||||
mark = element(2, lists:keyfind(mark, 1, Fields)),
|
mark = element(2, lists:keyfind(mark, 1, Fields)),
|
||||||
|
@ -32,12 +32,16 @@ list(Start, Length) -> ts_common:list(ts_user, Start, Length).
|
|||||||
|
|
||||||
hash_input_record(User=#ts_user{}) ->
|
hash_input_record(User=#ts_user{}) ->
|
||||||
% create a new User record
|
% create a new User record
|
||||||
{HashedPwd, Salt} = hash_pwd(User#ts_user.pwd),
|
Salt = generate_salt(),
|
||||||
|
HashedPwd = hash_pwd(User#ts_user.pwd, Salt),
|
||||||
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
|
User#ts_user{pwd = HashedPwd, pwd_salt = Salt}.
|
||||||
|
|
||||||
generate_salt() -> crypto:rand_bytes(36).
|
generate_salt() -> crypto:rand_bytes(36).
|
||||||
|
|
||||||
hash_pwd(Password) ->
|
hash_pwd(Password, Salt) -> crypto:sha(Password ++ Salt).
|
||||||
Salt = generate_salt(),
|
|
||||||
Hashed = crypto:sha(Password ++ Salt),
|
check_credentials(Username, Password) ->
|
||||||
{Hashed, Salt}.
|
User = lookup(Username),
|
||||||
|
HashedInput = hash_pwd(Password, User#ts_user.pwd_salt),
|
||||||
|
|
||||||
|
HashedInput == User#ts_user.pwd.
|
||||||
|
@ -5,6 +5,7 @@ runmod = timestamper
|
|||||||
|
|
||||||
<server timestamper-test>
|
<server timestamper-test>
|
||||||
port = 8000
|
port = 8000
|
||||||
listen = /home/jdbernard/projects/timestamper/web-app/www
|
listen = 127.0.0.1
|
||||||
appmods = timestamper_api
|
docroot = /home/jdbernard/projects/timestamper/web-app/www
|
||||||
|
appmods = ts_api
|
||||||
</server>
|
</server>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user