-module(eventhub_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> case infra_secrets:validate_startup() of ok -> start_application(); {error, Reason} -> io:format("~nFATAL: secret validation failed: ~p~n", [Reason]), {error, Reason} end. start_application() -> case infra_sup:start_link() of {ok, Pid} -> % Определяем список узлов кластера, если режим CLUSTER_MODE=true Nodes = case os:getenv("CLUSTER_MODE", "false") of "true" -> DnsName = os:getenv("DNS_NAME", "eventhub-node"), try inet:getaddrs(DnsName, inet) of {ok, IPs} when is_list(IPs), IPs /= [] -> % Получаем имена всех узлов Erlang, зарегистрированных в EPMD AllNodes = lists:flatmap(fun(IP) -> case erl_epmd:names(IP) of {ok, Names} -> [list_to_atom(Name ++ "@" ++ Name) || {Name, _Port} <- Names, lists:prefix("eventhub-node", Name)]; _ -> [] end end, IPs), % Исключаем свой узел, чтобы не подключаться к самому себе AllNodes -- [node()]; _ -> [] catch _:_ -> io:format("DNS lookup failed, starting as first node~n"), [] end; _ -> [] end, case Nodes of [] -> io:format("~nCluster: no nodes found or first node~n"); _ -> io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]), application:set_env(eventhub, extra_db_nodes, Nodes) end, ok = infra_mnesia:configure_dump_log(), application:ensure_all_started(mnesia), ok = infra_mnesia:verify_dump_log(), ok = infra_mnesia:init_tables(), case infra_mnesia:wait_for_tables() of ok -> ok; {error, WaitReason} -> error({mnesia_tables_not_ready, WaitReason}) end, ok = migration_engine:ensure_applied(), %% После wait + migrations: иначе backfill ловит {no_exists, user} на ещё не загруженных таблицах. ok = stats_collector:subscribe(), calendar_html_renderer:init_cache(), application:ensure_all_started(cowboy), start_http(), % Пользовательский API (8080) start_admin_http(), % Административный API (8445) start_swagger_http(), % Swagger UI и спецификация (8447) application:ensure_all_started(prometheus), application:ensure_all_started(prometheus_cowboy), case init_default_admins() of ok -> {ok, Pid}; {error, Reason} -> {error, Reason} end; Error -> Error end. stop(_State) -> ok. %% =================================================================== %% Пользовательский HTTP (порт 8080) — только публичные эндпоинты %% =================================================================== start_http() -> Port = get_env_int(http_port, 8080), Dispatch = cowboy_router:compile([ {'_', [ {"/metrics/[:registry]", prometheus_cowboy2_handler, []}, {"/health", handler_health, []}, {"/v1/register", handler_register, []}, {"/v1/verify", handler_verify, []}, {"/v1/login", handler_login, []}, {"/v1/refresh", handler_refresh, []}, {"/v1/user/me", handler_user_me, []}, {"/v1/user/bookings", handler_user_bookings, []}, {"/v1/user/reviews", handler_user_reviews, []}, {"/v1/search", handler_search, []}, {"/v1/calendars", handler_calendars, []}, {"/v1/calendars/:id", handler_calendar_by_id, []}, {"/v1/calendars/:calendar_id/view", handler_calendar_view, []}, {"/v1/calendars/:calendar_id/events", handler_events, []}, {"/v1/events/:id", handler_event_by_id, []}, {"/v1/events/:id/occurrences", handler_event_occurrences, []}, {"/v1/events/:id/occurrences/:start_time", handler_event_occurrences, []}, {"/v1/events/:id/bookings", handler_bookings, []}, {"/v1/bookings/:id", handler_booking_by_id, []}, {"/v1/reviews", handler_reviews, []}, {"/v1/reviews/:id", handler_review_by_id, []}, {"/v1/reports", handler_reports, []}, {"/v1/tickets", handler_tickets, []}, {"/v1/tickets/:id", handler_ticket_by_id, []}, {"/v1/subscription", handler_subscription, []} ]} %% 23 ]), Middlewares = [cowboy_router, cowboy_handler], Env = #{dispatch => Dispatch}, cowboy:start_clear(http, [{port, Port}], #{env => Env, middlewares => Middlewares, metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1, stream_handlers => [cowboy_metrics_h, cowboy_stream_h] }), io:format("HTTP server started on port ~p~n", [Port]). %% =================================================================== %% Административный HTTP (порт 8445) — все админские эндпоинты %% =================================================================== start_admin_http() -> PortAdmin = get_env_int(admin_http_port, 8445), Dispatch = cowboy_router:compile([ {'_', [ % ================== БАЗОВЫЕ ================== {"/admin/health", admin_handler_health, []}, {"/v1/admin/health", admin_handler_health, []}, {"/v1/admin/stats", admin_handler_stats, []}, {"/v1/admin/nodes/metrics", admin_handler_node_metrics, []}, {"/v1/admin/login", admin_handler_login, []}, {"/v1/admin/refresh", admin_handler_refresh, []}, % ================== ПОЛЬЗОВАТЕЛИ ================== {"/v1/admin/users", admin_handler_users, []}, {"/v1/admin/users/stats", admin_handler_user_stats, []}, {"/v1/admin/users/:id", admin_handler_user_by_id, []}, {"/v1/admin/users/:id/verification-token", admin_handler_user_verification_token, []}, % ================== КАЛЕНДАРИ ================== {"/v1/admin/calendars", admin_handler_calendars, []}, {"/v1/admin/calendars/stats", admin_handler_calendar_stats, []}, {"/v1/admin/calendars/:id", admin_handler_calendar_by_id, []}, % ================== СОБЫТИЯ ================== {"/v1/admin/events", admin_handler_events, []}, {"/v1/admin/events/stats", admin_handler_event_stats, []}, {"/v1/admin/events/:id", admin_handler_event_by_id, []}, % ================== ОТЧЁТЫ ================== {"/v1/admin/reports", admin_handler_reports, []}, {"/v1/admin/reports/stats", admin_handler_report_stats, []}, {"/v1/admin/reports/:id", admin_handler_report_by_id, []}, % ================== ОТЗЫВЫ ================== {"/v1/admin/reviews", admin_handler_reviews, []}, {"/v1/admin/reviews/stats", admin_handler_reviews_stats, []}, {"/v1/admin/reviews/:id", admin_handler_reviews_by_id, []}, % ================== БАН-СЛОВА ================== {"/v1/admin/banned-words/batch", admin_handler_banned_words, []}, {"/v1/admin/banned-words", admin_handler_banned_words, []}, {"/v1/admin/banned-words/:word", admin_handler_banned_words, []}, % ================== АВТОМОДЕРАЦИЯ ================== {"/v1/admin/automod/settings", admin_handler_automod, []}, {"/v1/admin/automod/hits", admin_handler_automod, []}, {"/v1/admin/automod/hits/:id", admin_handler_automod, []}, % ================== ТИКЕТЫ ================== {"/v1/admin/tickets/stats", admin_handler_ticket_stats, []}, {"/v1/admin/tickets/:id", admin_handler_ticket_by_id, []}, {"/v1/admin/tickets", admin_handler_tickets, []}, % ================== ПОДПИСКИ ================== {"/v1/admin/subscriptions", admin_handler_subscriptions, []}, {"/v1/admin/subscriptions/stats", admin_handler_subscription_stats, []}, {"/v1/admin/subscriptions/:id", admin_handler_subscriptions_by_id, []}, % ================== Управление ролями (только для superadmin) ================== {"/v1/admin/me", admin_handler_me, []}, {"/v1/admin/admins", admin_handler_admins, []}, {"/v1/admin/admins/:id", admin_handler_admins_by_id, []}, {"/v1/admin/audit", admin_handler_audit, []}, % ================== МОДЕРАЦИЯ (общий маршрут) ================== {"/v1/admin/:target_type/:id", admin_handler_moderation, []} ]} ]), Middlewares = [cowboy_router, cowboy_handler], Env = #{dispatch => Dispatch}, cowboy:start_clear(admin_http, [{port, PortAdmin}], #{env => Env, middlewares => Middlewares, metrics_callback => fun prometheus_cowboy2_instrumenter:observe/1, stream_handlers => [cowboy_metrics_h, cowboy_stream_h] }), io:format("Admin HTTP server started on port ~p~n", [PortAdmin]), % WebSocket для пользователей WsDispatch = cowboy_router:compile([{'_', [{"/ws", ws_handler, []}]}]), PortWs = get_env_int(ws_port, 8081), cowboy:start_clear(ws, [{port, PortWs}], #{env => #{dispatch => WsDispatch}}), % WebSocket для админов AdminWsDispatch = cowboy_router:compile([{'_', [ {"/admin/ws/metrics", admin_ws_handler, []}, {"/admin/ws", admin_ws_handler, []} ]}]), PortAdminWs = get_env_int(admin_ws_port, 8446), cowboy:start_clear(admin_ws, [{port, PortAdminWs}], #{env => #{dispatch => AdminWsDispatch}}), io:format("WebSocket started on ports ~p (user) and ~p (admin)~n", [PortWs, PortAdminWs]). %% =================================================================== %% Swagger HTTP (порт 8447) — документация API %% =================================================================== start_swagger_http() -> PortSwagger = get_env_int(swagger_http_port, 8447), Dispatch = cowboy_router:compile([ {'_', [ {"/", swagger_docs_handler, []}, {"/[...]", swagger_docs_handler, []} ]} ]), Middlewares = [cowboy_router, cowboy_handler], Env = #{dispatch => Dispatch}, cowboy:start_clear(swagger_http, [{port, PortSwagger}], #{env => Env, middlewares => Middlewares}), io:format("Swagger HTTP server started on port ~p~n", [PortSwagger]). %% ---------- Инициализация администраторов ---------- init_default_admins() -> case core_admin:list_all() of [] -> seed_default_admins(); _ -> io:format("Admins already exist. Skipping creation.~n"), ok end. seed_default_admins() -> Admins = [ {superadmin, "ADMIN_SUPER_EMAIL", "superadmin@eventhub.local", "ADMIN_SUPER_PASSWORD", "123456"}, {admin, "ADMIN_EMAIL", "admin@eventhub.local", "ADMIN_PASSWORD", "123456"}, {moderator, "ADMIN_MODER_EMAIL", "moderator@eventhub.local", "ADMIN_MODER_PASSWORD", "123456"}, {support, "ADMIN_SUPPORT_EMAIL", "support@eventhub.local", "ADMIN_SUPPORT_PASSWORD", "123456"} ], lists:foldl(fun seed_admin/2, ok, Admins). seed_admin({Role, EmailKey, DefaultEmail, PassKey, DefaultPass}, ok) -> case admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) of {error, _} = Err -> Err; {ok, {Email, Pass}} -> {ok, _} = core_admin:create(Email, Pass, Role), io:format("Default ~p created: ~s~n", [Role, Email]), ok end; seed_admin(_, Err) -> Err. admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) -> case infra_secrets:is_dev() of true -> {ok, {list_to_binary(os:getenv(EmailKey, DefaultEmail)), list_to_binary(os:getenv(PassKey, DefaultPass))}}; false -> case infra_secrets:require_admin_seed_email(EmailKey) of {error, _} = Err -> Err; {ok, Email} -> case infra_secrets:require_admin_seed_password(PassKey) of {error, _} = Err -> Err; {ok, Pass} -> {ok, {Email, Pass}} end end end. get_env_int(Key, Default) -> case application:get_env(eventhub, Key, Default) of Val when is_list(Val) -> list_to_integer(Val); Val when is_integer(Val) -> Val end.