- Instead of returning Meta-Data (status) in the header and the body, we now only return the content in the body, except in error when we return the error message. - JSON entries use the 'id' name for entity ids (instead of 'username', 'timeline_id', and 'entry_id')
569 lines
19 KiB
Erlang
569 lines
19 KiB
Erlang
-module(ts_api).
|
|
-compile(export_all).
|
|
|
|
-include("ts_db_records.hrl").
|
|
-include("yaws_api.hrl").
|
|
|
|
out(YArg) ->
|
|
% retreive the session data
|
|
Session = ts_api_session:get_session(YArg),
|
|
|
|
%get the app mod data
|
|
PathString = YArg#arg.appmoddata,
|
|
|
|
% split the path
|
|
PathElements = case PathString of
|
|
undefined -> []; %handle no end slash: /ts_api
|
|
_Any -> string:tokens(PathString, "/")
|
|
end,
|
|
|
|
% process the request
|
|
case catch dispatch_request(YArg, Session, PathElements) of
|
|
{'EXIT', Err} ->
|
|
% TODO: log error internally
|
|
error_logger:error_report("TimeStamper: ~p", [Err]),
|
|
make_json_500(YArg, Err);
|
|
Other -> Other
|
|
end.
|
|
|
|
% ================================== %
|
|
% ======== DISPATCH METHODS ======== %
|
|
% ================================== %
|
|
|
|
%% Entry point to the TimeStamper API dispatch system
|
|
dispatch_request(YArg, _Session, []) -> make_json_404(YArg, [{see_docs, "/ts_api_doc"}]);
|
|
|
|
dispatch_request(YArg, Session, [H|T]) ->
|
|
Param = path_element_to_atom(H),
|
|
|
|
case {Session, Param} of
|
|
{_, login} -> do_login(YArg);
|
|
{_, logout} -> do_logout(YArg);
|
|
|
|
{not_logged_in, _} -> make_json_401(YArg);
|
|
{session_expired, _} -> make_json_401(YArg, [{error, "session expired"}]);
|
|
|
|
{_S, app} -> dispatch_app(YArg, Session, T);
|
|
{_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.
|
|
|
|
% -------- Dispatch for /app -------- %
|
|
|
|
dispatch_app(YArg, Session, Params) ->
|
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
|
|
|
case {HTTPMethod, Params} of
|
|
|
|
{'GET', ["user_summary", UsernameStr]} ->
|
|
case {Session#ts_api_session.username,
|
|
path_element_to_atom(UsernameStr)} of
|
|
|
|
{Username, Username} -> get_user_summary(YArg, Username);
|
|
_ -> make_json_401(YArg)
|
|
end;
|
|
|
|
{_BadMethod, ["user_summary", _UsernameStr]} ->
|
|
make_json_405(YArg, [{see_docs, "/ts_api_docs/app.html"}]);
|
|
|
|
_Other -> make_json_404(YArg, [{see_docs, "/ts_api_docs/app.html"}])
|
|
|
|
end.
|
|
|
|
% -------- Dispatch for /user -------- %
|
|
|
|
dispatch_user(YArg, Session, []) ->
|
|
dispatch_user(YArg, Session, [atom_to_list(Session#ts_api_session.username)]);
|
|
|
|
dispatch_user(YArg, Session, [H]) ->
|
|
Username = path_element_to_atom(H),
|
|
HTTPMethod = (YArg#arg.req)#http_request.method,
|
|
|
|
% compare to the logged-in user
|
|
case {HTTPMethod, Session#ts_api_session.username} of
|
|
|
|
{'GET', Username} -> get_user(YArg, Username);
|
|
|
|
{_BadMethod, Username} ->
|
|
make_json_405(YArg, [{see_docs, "/ts_api_doc/users.html"}]);
|
|
|
|
_Other -> make_json_401(YArg, [{see_docs, "/ts_api_doc/users.html"}])
|
|
end.
|
|
|
|
% -------- Dispatch for /timeline -------- %
|
|
|
|
dispatch_timeline(YArg, _Session, []) ->
|
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]);
|
|
|
|
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.html"}])
|
|
end.
|
|
|
|
% just username, list timelines
|
|
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);
|
|
_Other -> make_json_405(YArg, [{see_docs, "/ts_api_doc/timelines.html"}])
|
|
end;
|
|
|
|
dispatch_timeline(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' -> get_timeline(YArg, Username, TimelineId);
|
|
'POST' -> post_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.html"}])
|
|
end;
|
|
|
|
dispatch_timeline(YArg, _Other) ->
|
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/timelines.html"}]).
|
|
|
|
% -------- Dispatch for /entry -------- %
|
|
|
|
dispatch_entry(YArg, _Session, []) ->
|
|
make_json_404(YArg, [{see_docs, "/ts_aip_doc/entries.html"}]);
|
|
|
|
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.html"}])
|
|
end.
|
|
|
|
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.html"}])
|
|
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.html"}])
|
|
end;
|
|
|
|
dispatch_entry(YArg, _Other) ->
|
|
make_json_404(YArg, [{see_docs, "/ts_api_doc/entries.html"}]).
|
|
|
|
% ============================== %
|
|
% ======== 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 and 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, [{error,
|
|
"bad username/password combination"}])
|
|
end;
|
|
|
|
_Other -> make_json_400(YArg, [{see_docs, "/ts_api_doc/login.html"}])
|
|
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),
|
|
{status, 200}.
|
|
|
|
get_user_summary(YArg, Username) ->
|
|
case ts_user:lookup(Username) of
|
|
no_record -> make_json_404(YArg);
|
|
User ->
|
|
EJSONUser = ts_json:record_to_ejson(User),
|
|
|
|
Timelines = ts_timeline:list(Username, 0, 100),
|
|
EJSONTimelines = {array,
|
|
lists:map(fun ts_json:record_to_ejson/1, Timelines)},
|
|
|
|
JSONResp = json:encode({struct,
|
|
[{user, EJSONUser},
|
|
{timelines, EJSONTimelines}
|
|
]}),
|
|
|
|
{content, "application/json", JSONResp}
|
|
end.
|
|
|
|
get_user(YArg, Username) ->
|
|
case ts_user:lookup(Username) of
|
|
no_record -> make_json_404(YArg);
|
|
User -> make_json_200(YArg, User)
|
|
end.
|
|
|
|
list_timelines(YArg, Username) ->
|
|
% pull out the POST data
|
|
QueryData = yaws_api:parse_query(YArg),
|
|
|
|
% read or default the Start
|
|
Start = case lists:keyfind(start, 1, QueryData) of
|
|
{start, StartVal} -> list_to_integer(StartVal);
|
|
false -> 0
|
|
end,
|
|
|
|
% read or default the Length
|
|
Length = case lists:keyfind(length, 1, QueryData) of
|
|
{length, LengthVal} ->
|
|
erlang:min(list_to_integer(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(EJSONTimelines),
|
|
|
|
% return response
|
|
{content, "application/json", JSONResponse}.
|
|
|
|
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, [{error, "no such timeline"}]);
|
|
% return the timeline data
|
|
Timeline -> make_json_200(YArg, Timeline)
|
|
end.
|
|
|
|
put_timeline(YArg, Username, TimelineId) ->
|
|
|
|
% parse the request body
|
|
EJSON = parse_json_body(YArg),
|
|
|
|
% parse into a Timeline record
|
|
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
|
|
case ts_timeline: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(ts_json:record_to_ejson(ExistingRecord)),
|
|
|
|
{content, "application/json", JSONResponse};
|
|
|
|
Error ->
|
|
error_logger:error_report("Unable to create a new timeline: ~p", [Error]),
|
|
make_json_500(YArg, Error)
|
|
end.
|
|
|
|
post_timeline(YArg, Username, TimelineId) ->
|
|
% parse the POST data
|
|
EJSON = parse_json_body(YArg),
|
|
%{struct, Fields} = EJSON,
|
|
|
|
% parse into a timeline record
|
|
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
|
|
ok -> make_json_200(YArg, NewRecord);
|
|
|
|
no_record -> make_json_404(YArg,
|
|
[{error, "no such timeline"},
|
|
{see_docs, "/ts_api_doc/timelines.html#POST"}]);
|
|
|
|
Error ->
|
|
error_logger:error_report("Unable update timeline: ~p", [Error]),
|
|
make_json_500(YArg, Error)
|
|
end.
|
|
|
|
delete_timeline(_YArg, _Username, _TimelineId) -> {status, 405}.
|
|
|
|
list_entries(YArg, Username, TimelineId) ->
|
|
% pull out the POST data
|
|
QueryData = yaws_api:parse_query(YArg),
|
|
|
|
% first determine if we are listing by date
|
|
case {ts_timeline:lookup(Username, TimelineId),
|
|
lists:keyfind("byDate", 1, QueryData)} of
|
|
|
|
{no_record, _ByDateField} -> make_json_404(
|
|
[{error, "no such timeline"},
|
|
{see_docs, "/ts_api_doc/entries.html#LIST"}]);
|
|
|
|
% listing by date range
|
|
{Timeline, {"byDate", "true"}} ->
|
|
|
|
% look for the start date; default to the beginning of the timeline
|
|
StartDate = case lists:keyfind("startDate", 1, QueryData) of
|
|
% TODO: error handling if the date is badly formatted
|
|
{startDate, StartDateVal} -> ts_json:decode_date(StartDateVal);
|
|
false -> Timeline#ts_timeline.created
|
|
end,
|
|
|
|
% look for end date; default to right now
|
|
EndDate = case lists:keyfind("endDate", 1, QueryData) of
|
|
% TODO: error handling if the date is badly formatted
|
|
{endDate, EndDateVal} -> ts_json:decode_date(EndDateVal);
|
|
false -> calendar:now_to_universal_time(erlang:now())
|
|
end,
|
|
|
|
% read sort order and list entries
|
|
Entries = case lists:keyfind("order", 1, QueryData) of
|
|
% descending sort order
|
|
{order, "desc"} -> ts_entry:list_desc(
|
|
{Username, TimelineId}, StartDate, EndDate);
|
|
|
|
% ascending order--{other, asc}--and default
|
|
_Other -> ts_entry:list_asc(
|
|
{Username, TimelineId}, StartDate, EndDate)
|
|
end,
|
|
|
|
EJSONEntries = {array, lists:map(
|
|
fun ts_json:record_to_ejson/1, Entries)},
|
|
|
|
JSONResponse = json:encode(EJSONEntries),
|
|
|
|
{content, "application/json", JSONResponse};
|
|
|
|
% listing by table position
|
|
_Other ->
|
|
|
|
% read or default the Start
|
|
Start = case lists:keyfind("start", 1, QueryData) of
|
|
{"start", StartVal} -> list_to_integer(StartVal);
|
|
false -> 0
|
|
end,
|
|
|
|
% read or default the Length
|
|
Length = case lists:keyfind("length", 1, QueryData) of
|
|
{"length", LengthVal} ->
|
|
erlang:min(list_to_integer(LengthVal), 500);
|
|
false -> 50
|
|
end,
|
|
|
|
% read sort order and list entries
|
|
Entries = case lists:keyfind("order", 1, QueryData) of
|
|
{"order", "desc"} -> ts_entry:list_desc(
|
|
{Username, TimelineId}, Start, Length);
|
|
|
|
_UnknownOrder -> ts_entry:list_asc(
|
|
{Username, TimelineId}, Start, Length)
|
|
end,
|
|
|
|
EJSONEntries = {array, lists:map(
|
|
fun ts_json:record_to_ejson/1, Entries)},
|
|
|
|
JSONResponse = json:encode(EJSONEntries),
|
|
|
|
{content, "application/json", JSONResponse}
|
|
end.
|
|
|
|
get_entry(YArg, Username, TimelineId, EntryId) ->
|
|
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
|
% no such entry
|
|
no_record -> make_json_404(YArg, [{error, "no such entry"}]);
|
|
% return the entry data
|
|
Entry -> make_json_200(YArg, Entry)
|
|
end.
|
|
|
|
put_entry(YArg, Username, TimelineId) ->
|
|
|
|
% parse the request body
|
|
EJSON = parse_json_body(YArg),
|
|
|
|
% parse into ts_entry record
|
|
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
|
|
% record created
|
|
{ok, CreatedRecord} ->
|
|
[{status, 201}, make_json_200(YArg, CreatedRecord)];
|
|
|
|
% will not create, record exists
|
|
{error, {record_exists, ExistingRecord}} ->
|
|
JSONResponse = json:encode(ts_json:record_to_ejson(ExistingRecord)),
|
|
|
|
{content, "application/json", JSONResponse};
|
|
|
|
OtherError ->
|
|
error_logger:error_report("TimeStamper: Could not create entry: ~p", [OtherError]),
|
|
make_json_500(YArg, OtherError)
|
|
end.
|
|
|
|
post_entry(YArg, Username, TimelineId, EntryId) ->
|
|
|
|
% parse the POST data
|
|
EJSON = parse_json_body(YArg),
|
|
|
|
% parse into ts_entry record
|
|
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
|
|
ok -> make_json_200(YArg, NewRecord);
|
|
|
|
no_record -> make_json_404(YArg,
|
|
[{status, "no such entry"}, {see_docs, "/ts_api_doc/entries.html#POST"}]);
|
|
|
|
Error ->
|
|
error_logger:error_report("TimeStamper: Unable to update entry: ~p", [Error]),
|
|
make_json_500(YArg, Error)
|
|
end.
|
|
|
|
delete_entry(YArg, Username, TimelineId, EntryId) ->
|
|
|
|
% find the record to delete
|
|
case ts_entry:lookup(Username, TimelineId, EntryId) of
|
|
|
|
no_record -> make_json_404(YArg);
|
|
|
|
Record ->
|
|
% try to delete
|
|
case ts_entry:delete(Record) of
|
|
ok -> {status, 200};
|
|
Error ->
|
|
error_logger:error_report("Error occurred deleting entry record: ~p", [Error]),
|
|
make_json_500(YArg, Error)
|
|
end
|
|
end.
|
|
|
|
% ============================== %
|
|
% ======== UTIL METHODS ======== %
|
|
% ============================== %
|
|
|
|
%% Convert one path element to an atom.
|
|
path_element_to_atom(PE) ->
|
|
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
|
|
error_logger:error_report("Error parsing JSON request body: ~p", [Error]),
|
|
throw(make_json_400(YArg))
|
|
end.
|
|
|
|
%% Create a JSON 200 response.
|
|
make_json_200(_YArg, Record) ->
|
|
JSONResponse = json:encode(ts_json:record_to_ejson(Record)),
|
|
{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.
|
|
make_json_404(YArg) -> make_json_404(YArg, []).
|
|
make_json_404(YArg, Fields) ->
|
|
% add default status if not provided
|
|
F1 = case lists:keyfind(status, 1, Fields) of
|
|
false -> Fields ++ [{status, "not found"}];
|
|
_Else -> Fields
|
|
end,
|
|
|
|
% add the path they requested
|
|
F2 = F1 ++ [{path, element(2, (YArg#arg.req)#http_request.path)}],
|
|
|
|
[{status, 404}, {content, "application/json", json:encode({struct, F2})}].
|
|
|
|
make_json_405(YArg) -> make_json_405(YArg, []).
|
|
make_json_405(YArg, Fields) ->
|
|
% add default status if not provided
|
|
F1 = case lists:keyfind(status, 1, Fields) of
|
|
false -> Fields ++ [{status, method_not_allowed}];
|
|
_Else -> Fields
|
|
end,
|
|
|
|
% add the path they requested
|
|
F2 = F1 ++ [{path, (YArg#arg.req)#http_request.path}],
|
|
|
|
[{status, 405}, {content, "application/json", json:encode({struct, F2})}].
|
|
|
|
make_json_500(_YArg, Error) ->
|
|
EJSON = {struct, [
|
|
{status, "internal server error"},
|
|
{error, io_lib:format("~p", [Error])}]},
|
|
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
|
|
|
|
make_json_500(_YArg) ->
|
|
EJSON = {struct, [
|
|
{status, "internal server error"}]},
|
|
[{status, 500}, {content, "application/json", json:encode(EJSON)}].
|