%%%----------------------------------------------------------------------------- %%% @copyright (C) 2011-2019, 2600Hz %%% @doc Provision template module. %%% Handle client requests for provisioner template documents. %%% %%%
Regarding storing the template as an attachment: %%% Since the template is a 300k JSON object it is more efficient to store it as %%% an attachment, funky I know but necessary. Also since we already require %%% two API calls for editing a template we will maintain backward compatibility by %%% not requiring an additional API call for the template and merge/unmerge it %%% from requests
. %%% %%% %%% @author Jon Blanton %%% @author Karl Anderson %%% @author Pierre Fenoll %%% @author James Aimonetti %%% @end %%%----------------------------------------------------------------------------- -module(cb_local_provisioner_templates). -export([init/0 ,content_types_provided/3, content_types_accepted/3 ,allowed_methods/0, allowed_methods/1, allowed_methods/2 ,resource_exists/0, resource_exists/1, resource_exists/2 ,validate/1, validate/2, validate/3 ,put/1 ,post/2 ,delete/2 ,put/3 ,post/3 ,delete/3 ]). -include("crossbar.hrl"). -define(MOD_CONFIG_CAT, <<(?CONFIG_CAT)/binary, ".provisioner_templates">>). -define(CB_LIST, <<"provisioner_templates/crossbar_listing">>). -define(IMAGE_REQ, <<"image">>). -define(TEMPLATE_ATTCH, <<"template">>). -define(MIME_TYPES, [{<<"image">>, <<"*">>} ,{<<"application">>, <<"octet-stream">>} | ?BASE64_CONTENT_TYPES ]). %%%============================================================================= %%% API %%%============================================================================= %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec init() -> 'ok'. init() -> _ = crossbar_bindings:bind(<<"*.content_types_provided.local_provisioner_templates">>, ?MODULE, 'content_types_provided'), _ = crossbar_bindings:bind(<<"*.content_types_accepted.local_provisioner_templates">>, ?MODULE, 'content_types_accepted'), _ = crossbar_bindings:bind(<<"*.allowed_methods.local_provisioner_templates">>, ?MODULE, 'allowed_methods'), _ = crossbar_bindings:bind(<<"*.resource_exists.local_provisioner_templates">>, ?MODULE, 'resource_exists'), _ = crossbar_bindings:bind(<<"*.validate.local_provisioner_templates">>, ?MODULE, 'validate'), _ = crossbar_bindings:bind(<<"*.execute.put.local_provisioner_templates">>, ?MODULE, 'put'), _ = crossbar_bindings:bind(<<"*.execute.post.local_provisioner_templates">>, ?MODULE, 'post'), _ = crossbar_bindings:bind(<<"*.execute.delete.local_provisioner_templates">>, ?MODULE, 'delete'), 'ok'. %%------------------------------------------------------------------------------ %% @doc Add content types provided by this module %% @end %%------------------------------------------------------------------------------ -spec content_types_provided(cb_context:context(), path_token(), path_token()) -> cb_context:context(). content_types_provided(Context, PT1, PT2) -> content_types_provided_for_provisioner(Context, PT1, PT2, cb_context:req_verb(Context)). -spec content_types_provided_for_provisioner(cb_context:context(), path_token(), path_token(), http_method()) -> cb_context:context(). content_types_provided_for_provisioner(Context, DocId, ?IMAGE_REQ, ?HTTP_GET) -> Db = kz_util:format_account_id(cb_context:auth_account_id(Context), 'encoded'), case kz_datamgr:open_doc(Db, DocId) of {'error', _} -> Context; {'ok', JObj} -> CT = kz_doc:attachment_content_type(JObj, ?IMAGE_REQ, <<"application/octet-stream">>), [Type, SubType] = binary:split(CT, <<"/">>), lager:debug("found attachment of content type: ~s/~s~n", [Type, SubType]), cb_context:set_content_types_provided(Context, [{'to_binary', [{Type, SubType}]}]) end; content_types_provided_for_provisioner(Context, _, _, _) -> Context. %%------------------------------------------------------------------------------ %% @doc Add content types accepted by this module %% @end %%------------------------------------------------------------------------------ -spec content_types_accepted(cb_context:context(), path_token(), path_token()) -> cb_context:context(). content_types_accepted(Context, PT1, PT2) -> content_types_accepted(Context, PT1, PT2, cb_context:req_verb(Context)). -spec content_types_accepted(cb_context:context(), path_token(), path_token(), http_method()) -> cb_context:context(). content_types_accepted(Context, _, ?IMAGE_REQ, ?HTTP_PUT) -> cb_context:set_content_types_accepted(Context, [{'from_binary', ?MIME_TYPES}]); content_types_accepted(Context, _, ?IMAGE_REQ, ?HTTP_POST) -> cb_context:set_content_types_accepted(Context, [{'from_binary', ?MIME_TYPES}]); content_types_accepted(Context, _, _, _) -> Context. %%------------------------------------------------------------------------------ %% @doc This function determines the verbs that are appropriate for the %% given Nouns. For example `/accounts/' can only accept `GET' and `PUT'. %% %% Failure here returns `405 Method Not Allowed'. %% @end %%------------------------------------------------------------------------------ -spec allowed_methods() -> http_methods(). allowed_methods() -> [?HTTP_GET, ?HTTP_PUT]. -spec allowed_methods(path_token()) -> http_methods(). allowed_methods(_TemplateId) -> [?HTTP_GET, ?HTTP_POST, ?HTTP_DELETE]. -spec allowed_methods(path_token(), path_token()) -> http_methods(). allowed_methods(_TemplateId, ?IMAGE_REQ) -> [?HTTP_GET, ?HTTP_POST, ?HTTP_DELETE]. %%------------------------------------------------------------------------------ %% @doc This function determines if the provided list of Nouns are valid. %% Failure here returns `404 Not Found'. %% @end %%------------------------------------------------------------------------------ -spec resource_exists() -> 'true'. resource_exists() -> 'true'. -spec resource_exists(path_token()) -> 'true'. resource_exists(_) -> 'true'. -spec resource_exists(path_token(), path_token()) -> 'true'. resource_exists(_, ?IMAGE_REQ) -> 'true'. %%------------------------------------------------------------------------------ %% @doc This function determines if the parameters and content are correct %% for this request %% %% Failure here returns 400. %% @end %%------------------------------------------------------------------------------ -spec validate(cb_context:context()) -> cb_context:context(). validate(Context) -> validate_verb(Context, cb_context:req_verb(Context)). -spec validate(cb_context:context(), path_token()) -> cb_context:context(). validate(Context, PT1) -> validate_verb(Context, PT1, cb_context:req_verb(Context)). -spec validate(cb_context:context(), path_token(), path_token()) -> cb_context:context(). validate(Context, PT1, PT2) -> validate_verb(Context, PT1, PT2, cb_context:req_verb(Context)). -spec validate_verb(cb_context:context(), http_method()) -> cb_context:context(). validate_verb(Context, ?HTTP_GET) -> load_provisioner_template_summary(Context); validate_verb(Context, ?HTTP_PUT) -> create_provisioner_template(Context). -spec validate_verb(cb_context:context(), path_token(), http_method()) -> cb_context:context(). validate_verb(Context, DocId, ?HTTP_GET) -> load_provisioner_template(DocId, Context); validate_verb(Context, DocId, ?HTTP_POST) -> update_provisioner_template(DocId, Context); validate_verb(Context, DocId, ?HTTP_DELETE) -> load_provisioner_template(DocId, Context). -spec validate_verb(cb_context:context(), path_token(), path_token(), http_method()) -> cb_context:context(). validate_verb(Context, DocId, ?IMAGE_REQ, ?HTTP_GET) -> load_template_image(DocId, Context); validate_verb(Context, _DocId, ?IMAGE_REQ, ?HTTP_PUT) -> upload_template_image(Context); validate_verb(Context, _DocId, ?IMAGE_REQ, ?HTTP_POST) -> upload_template_image(Context); validate_verb(Context, DocId, ?IMAGE_REQ, ?HTTP_DELETE) -> load_template_image(DocId, Context). -spec post(cb_context:context(), path_token()) -> cb_context:context(). post(Context, DocId) -> %% see note at top of file JObj = cb_context:doc(Context), Template = kz_json:get_value(<<"template">>, JObj), Doc = kz_json:delete_key(<<"template">>, JObj), Context1 = crossbar_doc:save(cb_context:set_doc(Context, Doc)), case cb_context:resp_status(Context1) of 'success' -> SavedResp = cb_context:resp_data(Context1), Opts = [{'content_type', <<"application/json">>} | ?TYPE_CHECK_OPTION(<<"provisioner_template">>)], Ctx2 = crossbar_doc:save_attachment(DocId, ?TEMPLATE_ATTCH, kz_json:encode(Template), Context, Opts), case cb_context:resp_status(Ctx2) of 'success' -> cb_context:set_resp_data(Context1, kz_json:set_value(<<"template">>, Template, SavedResp)); _Error -> Ctx2 end; _Error -> Context1 end. -spec put(cb_context:context()) -> cb_context:context(). put(Context) -> %% see note at top of file JObj = cb_context:doc(Context), Template = kz_json:get_value(<<"template">>, JObj), Doc = kz_json:delete_key(<<"template">>, JObj), Context1 = crossbar_doc:save(cb_context:set_doc(Context, Doc)), case cb_context:resp_status(Context1) of 'success' -> SavedDoc = cb_context:doc(Context1), SavedResp = cb_context:resp_data(Context1), DocId = kz_doc:id(SavedDoc), Opts = [{'content_type', <<"application/json">>} | ?TYPE_CHECK_OPTION(<<"provisioner_template">>)], Ctx2 = crossbar_doc:save_attachment(DocId, ?TEMPLATE_ATTCH, kz_json:encode(Template), Context, Opts), case cb_context:resp_status(Ctx2) of 'success' -> cb_context:set_resp_data(Context1, kz_json:set_value(<<"template">>, Template, SavedResp)); Else -> Else end; Else -> Else end. -spec delete(cb_context:context(), path_token()) -> cb_context:context(). delete(Context, _) -> crossbar_doc:delete(Context). -spec post(cb_context:context(), path_token(), path_token()) -> cb_context:context(). post(Context, DocId, ?IMAGE_REQ) -> [{_, JObj}] = cb_context:req_files(Context), Contents = kz_json:get_value(<<"contents">>, JObj), CT = kz_json:get_value([<<"headers">>, <<"content_type">>], JObj, <<"application/octet-stream">>), Opts = [{'content_type', CT} | ?TYPE_CHECK_OPTION(<<"provisioner_template">>)], crossbar_doc:save_attachment(DocId, ?IMAGE_REQ, Contents, Context, Opts). -spec put(cb_context:context(), path_token(), path_token()) -> cb_context:context(). put(Context, DocId, ?IMAGE_REQ) -> [{_, JObj}] = cb_context:req_files(Context), Contents = kz_json:get_value(<<"contents">>, JObj), CT = kz_json:get_value([<<"headers">>, <<"content_type">>], JObj, <<"application/octet-stream">>), Opts = [{'content_type', CT} | ?TYPE_CHECK_OPTION(<<"provisioner_template">>)], crossbar_doc:save_attachment(DocId, ?IMAGE_REQ, Contents, Context, Opts). -spec delete(cb_context:context(), path_token(), path_token()) -> cb_context:context(). delete(Context, DocId, ?IMAGE_REQ) -> crossbar_doc:delete_attachment(DocId, ?IMAGE_REQ, Context). %%%============================================================================= %%% Internal functions %%%============================================================================= %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec load_template_image(path_token(), cb_context:context()) -> cb_context:context(). load_template_image(DocId, Context) -> crossbar_doc:load_attachment(DocId, ?IMAGE_REQ, ?TYPE_CHECK_OPTION(<<"provisioner_template">>), Context). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec upload_template_image(cb_context:context()) -> cb_context:context(). upload_template_image(Context) -> upload_template_image(Context, cb_context:req_files(Context)). -spec upload_template_image(cb_context:context(), req_files()) -> cb_context:context(). upload_template_image(Context, []) -> cb_context:add_validation_error(<<"file">> ,<<"required">> ,kz_json:from_list([{<<"message">>, <<"please provide an image file">>}]) ,Context ); upload_template_image(Context, [{_, _}]) -> crossbar_util:response(kz_json:new(), Context); upload_template_image(Context, [_|_]) -> cb_context:add_validation_error(<<"file">> ,<<"maxItems">> ,kz_json:from_list([{<<"message">>, <<"Please provide a single image file">>}]) ,Context ). %%------------------------------------------------------------------------------ %% @doc Attempt to load list of provision templates, each summarized. Or a specific %% provision template summary. %% @end %%------------------------------------------------------------------------------ -spec load_provisioner_template_summary(cb_context:context()) -> cb_context:context(). load_provisioner_template_summary(Context) -> crossbar_doc:load_view(?CB_LIST, [], Context, fun normalize_view_results/2). %%------------------------------------------------------------------------------ %% @doc Create a new provision template document with the data provided, if it is valid %% @end %%------------------------------------------------------------------------------ -spec create_provisioner_template(cb_context:context()) -> cb_context:context(). create_provisioner_template(Context) -> OnSuccess = fun(C) -> on_successful_validation('undefined', C) end, cb_context:validate_request_data(<<"provisioner_templates">>, Context, OnSuccess). %%------------------------------------------------------------------------------ %% @doc Load a provision template document from the database %% @end %%------------------------------------------------------------------------------ -spec load_provisioner_template(kz_term:ne_binary(), cb_context:context()) -> cb_context:context(). load_provisioner_template(DocId, Context) -> %% see note at top of file Context1 = crossbar_doc:load(DocId, Context, ?TYPE_CHECK_OPTION(<<"provisioner_template">>)), case cb_context:resp_status(Context1) of 'success' -> RespJObj = cb_context:resp_data(Context1), Ctx2 = crossbar_doc:load_attachment(DocId, ?TEMPLATE_ATTCH, ?TYPE_CHECK_OPTION(<<"provisioner_template">>), Context), case cb_context:resp_status(Ctx2) of 'success' -> Template = kz_json:decode(cb_context:resp_data(Ctx2)), cb_context:set_resp_data(Context1, kz_json:set_value(<<"template">>, Template, RespJObj)); Else -> Else end; Else -> Else end. %%------------------------------------------------------------------------------ %% @doc Update an existing provision template document with the data provided, if it is %% valid %% @end %%------------------------------------------------------------------------------ -spec update_provisioner_template(kz_term:ne_binary(), cb_context:context()) -> cb_context:context(). update_provisioner_template(DocId, Context) -> OnSuccess = fun(C) -> on_successful_validation(DocId, C) end, cb_context:validate_request_data(<<"provisioner_templates">>, Context, OnSuccess). %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ -spec on_successful_validation(kz_term:api_binary(), cb_context:context()) -> cb_context:context(). on_successful_validation('undefined', Context) -> Doc = kz_json:set_values([{<<"pvt_type">>, <<"provisioner_template">>} ,{<<"pvt_provider">>, <<"provisioner.net">>} ,{<<"pvt_provisioner_type">>, <<"local">>} ] ,cb_context:doc(Context) ), case provisioner_util:get_provision_defaults(Doc) of {'ok', Defaults} -> cb_context:setters(Context ,[{fun cb_context:set_doc/2, Defaults} ,{fun cb_context:set_resp_status/2, 'success'} ]); {'error', Msg} -> crossbar_util:response('error', Msg, 500, Context) end; on_successful_validation(DocId, Context) -> crossbar_doc:load_merge(DocId, Context, ?TYPE_CHECK_OPTION(<<"provisioner_template">>)). %%------------------------------------------------------------------------------ %% @doc Normalizes the results of a view. %% @end %%------------------------------------------------------------------------------ -spec normalize_view_results(kz_json:object(), kz_json:objects()) -> kz_json:objects(). normalize_view_results(JObj, Acc) -> [kz_json:get_value(<<"value">>, JObj) | Acc].