feat(calendar): validate org defaults in calendar.settings
Documented keys default_location, default_duration_minutes, default_recurrence; unknown keys (week_patterns) passthrough; invalid known shape returns 400. Refs EventHub/EventHubBack#63
This commit is contained in:
@@ -154,6 +154,9 @@ update_calendar(Req) ->
|
||||
handler_utils:send_error(Req2, 402, <<"Subscription required for commercial calendar">>);
|
||||
{error, not_found} ->
|
||||
handler_utils:send_error(Req2, 404, <<"Calendar not found">>);
|
||||
{error, {invalid_settings, Key}} when is_binary(Key) ->
|
||||
handler_utils:send_error(Req2, 400,
|
||||
<<"Invalid settings.", Key/binary>>);
|
||||
{error, _} ->
|
||||
handler_utils:send_error(Req2, 500, <<"Internal server error">>)
|
||||
end;
|
||||
|
||||
+110
-17
@@ -4,6 +4,7 @@
|
||||
-export([create_calendar/3, create_calendar/4, create_calendar/5, get_calendar/2, list_calendars/1,
|
||||
update_calendar/3, delete_calendar/2, ensure_default_calendar/1]).
|
||||
-export([can_access/2, can_edit/2, booking_open/1]).
|
||||
-export([normalize_settings/1]).
|
||||
-export([admin_list_all/0, admin_get_by_id/1, admin_update/2, admin_delete/1]).
|
||||
|
||||
%% Создание календаря с политикой по умолчанию (manual)
|
||||
@@ -109,12 +110,16 @@ update_calendar(UserId, CalendarId, Updates) ->
|
||||
{ok, Calendar} ->
|
||||
case can_edit(UserId, Calendar) of
|
||||
true ->
|
||||
ValidUpdates = validate_updates(Updates),
|
||||
case gate_type_change(UserId, Calendar, ValidUpdates) of
|
||||
case validate_updates(Updates) of
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
{ok, ValidUpdates2} ->
|
||||
apply_calendar_update(CalendarId, Calendar, ValidUpdates2)
|
||||
{ok, ValidUpdates} ->
|
||||
case gate_type_change(UserId, Calendar, ValidUpdates) of
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
{ok, ValidUpdates2} ->
|
||||
apply_calendar_update(CalendarId, Calendar, ValidUpdates2)
|
||||
end
|
||||
end;
|
||||
false ->
|
||||
{error, access_denied}
|
||||
@@ -225,24 +230,41 @@ can_edit(_UserId, #calendar{owner_id = _OwnerId}) ->
|
||||
can_edit(_, _) ->
|
||||
false.
|
||||
|
||||
%% Валидация полей обновления
|
||||
%% Валидация полей обновления.
|
||||
%% Known settings keys with invalid shape → {error, {invalid_settings, Key}}.
|
||||
%% Unknown settings keys (e.g. week_patterns) are preserved as-is.
|
||||
validate_updates(Updates) ->
|
||||
lists:filtermap(fun(U) ->
|
||||
case normalize_update(U) of
|
||||
false -> false;
|
||||
Norm ->
|
||||
case validate_update(Norm) of
|
||||
true -> {true, Norm};
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
end, Updates).
|
||||
validate_updates(Updates, []).
|
||||
|
||||
validate_updates([], Acc) ->
|
||||
{ok, lists:reverse(Acc)};
|
||||
validate_updates([U | Rest], Acc) ->
|
||||
case normalize_update(U) of
|
||||
false ->
|
||||
validate_updates(Rest, Acc);
|
||||
{error, _} = Err ->
|
||||
Err;
|
||||
Norm ->
|
||||
case validate_update(Norm) of
|
||||
true -> validate_updates(Rest, [Norm | Acc]);
|
||||
false -> validate_updates(Rest, Acc)
|
||||
end
|
||||
end.
|
||||
|
||||
normalize_update({type, <<"personal">>}) -> {type, personal};
|
||||
normalize_update({type, <<"commercial">>}) -> {type, commercial};
|
||||
normalize_update({type, personal}) -> {type, personal};
|
||||
normalize_update({type, commercial}) -> {type, commercial};
|
||||
normalize_update({<<"settings">>, Value}) when is_map(Value) -> {settings, Value};
|
||||
normalize_update({<<"settings">>, Value}) when is_map(Value) ->
|
||||
case normalize_settings(Value) of
|
||||
{ok, Map} -> {settings, Map};
|
||||
{error, _} = Err -> Err
|
||||
end;
|
||||
normalize_update({settings, Value}) when is_map(Value) ->
|
||||
case normalize_settings(Value) of
|
||||
{ok, Map} -> {settings, Map};
|
||||
{error, _} = Err -> Err
|
||||
end;
|
||||
normalize_update({<<"title">>, Value}) when is_binary(Value) -> {title, Value};
|
||||
normalize_update({<<"description">>, Value}) when is_binary(Value) -> {description, Value};
|
||||
normalize_update({<<"tags">>, Value}) when is_list(Value) -> {tags, Value};
|
||||
@@ -261,7 +283,7 @@ validate_update({short_name, Value}) when is_binary(Value) -> true;
|
||||
validate_update({category, Value}) when is_binary(Value) -> true;
|
||||
validate_update({color, Value}) when is_binary(Value) -> true;
|
||||
validate_update({image_url, Value}) when is_binary(Value) -> true;
|
||||
%% Opaque JSON object for client prefs (e.g. week_patterns for schedule fill).
|
||||
%% Documented org-default keys validated in normalize_settings/1; unknown keys passthrough.
|
||||
validate_update({settings, Value}) when is_map(Value) -> true;
|
||||
validate_update({confirmation, Value}) ->
|
||||
case Value of
|
||||
@@ -272,6 +294,77 @@ validate_update({confirmation, Value}) ->
|
||||
end;
|
||||
validate_update(_) -> false.
|
||||
|
||||
%%%-------------------------------------------------------------------
|
||||
%%% @doc Validate documented calendar.settings keys for org defaults.
|
||||
%%% Unknown keys are kept (forward-compat, e.g. week_patterns).
|
||||
%%% @end
|
||||
%%%-------------------------------------------------------------------
|
||||
-spec normalize_settings(map()) -> {ok, map()} | {error, {invalid_settings, binary()}}.
|
||||
normalize_settings(Map) when is_map(Map) ->
|
||||
normalize_settings_keys(maps:keys(Map), Map).
|
||||
|
||||
normalize_settings_keys([], Map) ->
|
||||
{ok, Map};
|
||||
normalize_settings_keys([Key | Rest], Map) ->
|
||||
case validate_settings_key(Key, maps:get(Key, Map)) of
|
||||
ok ->
|
||||
normalize_settings_keys(Rest, Map);
|
||||
{error, _} = Err ->
|
||||
Err
|
||||
end.
|
||||
|
||||
validate_settings_key(<<"default_location">>, Val) ->
|
||||
validate_default_location(Val);
|
||||
validate_settings_key(<<"default_duration_minutes">>, Val) ->
|
||||
validate_default_duration(Val);
|
||||
validate_settings_key(<<"default_recurrence">>, Val) ->
|
||||
validate_default_recurrence(Val);
|
||||
validate_settings_key(_Unknown, _Val) ->
|
||||
ok.
|
||||
|
||||
validate_default_location(Loc) when is_map(Loc) ->
|
||||
case maps:get(<<"address">>, Loc, undefined) of
|
||||
Address when is_binary(Address), byte_size(Address) > 0 ->
|
||||
HasLat = maps:is_key(<<"lat">>, Loc),
|
||||
HasLon = maps:is_key(<<"lon">>, Loc),
|
||||
case {HasLat, HasLon} of
|
||||
{false, false} ->
|
||||
ok;
|
||||
{true, true} ->
|
||||
Lat = maps:get(<<"lat">>, Loc),
|
||||
Lon = maps:get(<<"lon">>, Loc),
|
||||
case is_number(Lat) andalso is_number(Lon) of
|
||||
true -> ok;
|
||||
false -> {error, {invalid_settings, <<"default_location">>}}
|
||||
end;
|
||||
_ ->
|
||||
{error, {invalid_settings, <<"default_location">>}}
|
||||
end;
|
||||
_ ->
|
||||
{error, {invalid_settings, <<"default_location">>}}
|
||||
end;
|
||||
validate_default_location(_) ->
|
||||
{error, {invalid_settings, <<"default_location">>}}.
|
||||
|
||||
validate_default_duration(N) when is_integer(N), N >= 1, N =< 1440 ->
|
||||
ok;
|
||||
validate_default_duration(_) ->
|
||||
{error, {invalid_settings, <<"default_duration_minutes">>}}.
|
||||
|
||||
validate_default_recurrence(null) ->
|
||||
ok;
|
||||
validate_default_recurrence(Rec) when is_map(Rec) ->
|
||||
Enabled = maps:get(<<"enabled">>, Rec, undefined),
|
||||
Freq = maps:get(<<"freq">>, Rec, undefined),
|
||||
Interval = maps:get(<<"interval">>, Rec, undefined),
|
||||
ValidFreq = Freq =:= <<"DAILY">> orelse Freq =:= <<"WEEKLY">> orelse Freq =:= <<"MONTHLY">>,
|
||||
case is_boolean(Enabled) andalso ValidFreq andalso is_integer(Interval) andalso Interval >= 1 of
|
||||
true -> ok;
|
||||
false -> {error, {invalid_settings, <<"default_recurrence">>}}
|
||||
end;
|
||||
validate_default_recurrence(_) ->
|
||||
{error, {invalid_settings, <<"default_recurrence">>}}.
|
||||
|
||||
%% ─── Административные функции ────────────────────────────────────────
|
||||
-spec admin_list_all() -> {ok, [map()]} | {error, term()}.
|
||||
admin_list_all() ->
|
||||
|
||||
Reference in New Issue
Block a user