feat(calendar): validate org defaults in calendar.settings
CI / test (push) Successful in 7m30s
CI / deploy-ift (push) Successful in 3m46s
CI / e2e-ift (push) Successful in 1m28s
CI / deploy-stage (push) Successful in 2m11s
CI / e2e-stage (push) Successful in 1m15s

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:
2026-07-29 22:50:31 +03:00
parent 4c480f1c43
commit 4986baf006
4 changed files with 191 additions and 19 deletions
+3
View File
@@ -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
View File
@@ -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() ->