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:
Jonathan Bernard 2011-02-07 08:56:07 -06:00
parent 5809ed3959
commit 0642c18a6e
12 changed files with 277 additions and 134 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

5
login.post Normal file
View 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
View 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.

View File

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

View File

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

View File

@ -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) ->

View File

@ -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)),

View File

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

View File

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