-module(eventhub_app). -behaviour(application). -export([start/2, stop/1]). start(_StartType, _StartArgs) -> pg:start_link(), 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, application:ensure_all_started(mnesia), ok = infra_mnesia:init_tables(), ok = infra_mnesia:wait_for_tables(), 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), init_default_admins(), {ok, Pid}; 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/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/stats", admin_handler_stats, []}, {"/v1/admin/login", admin_handler_login, []}, % ================== ПОЛЬЗОВАТЕЛИ ================== {"/v1/admin/users", admin_handler_users, []}, {"/v1/admin/users/:id", admin_handler_user_by_id, []}, % ================== СОБЫТИЯ ================== {"/v1/admin/events", admin_handler_events, []}, {"/v1/admin/events/:id", admin_handler_event_by_id, []}, % ================== ОТЧЁТЫ ================== {"/v1/admin/reports", admin_handler_reports, []}, {"/v1/admin/reports/:id", admin_handler_report_by_id, []}, % ================== ОТЗЫВЫ ================== {"/v1/admin/reviews", admin_handler_reviews, []}, {"/v1/admin/reviews/:id", admin_handler_reviews_by_id, []}, % ================== БАН-СЛОВА ================== {"/v1/admin/banned-words", admin_handler_banned_words, []}, {"/v1/admin/banned-words/:word", admin_handler_banned_words, []}, % ================== ТИКЕТЫ ================== {"/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/: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", 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 [] -> % Суперадмин SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")), SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")), {ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin), io:format("Default superadmin created: ~s~n", [SuperEmail]), % Админ AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")), AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")), {ok, _} = core_admin:create(AdminEmail, AdminPass, admin), io:format("Default admin created: ~s~n", [AdminEmail]), % Модератор ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")), ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")), {ok, _} = core_admin:create(ModerEmail, ModerPass, moderator), io:format("Default moderator created: ~s~n", [ModerEmail]), % Поддержка SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")), SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")), {ok, _} = core_admin:create(SupportEmail, SupportPass, support), io:format("Default support created: ~s~n", [SupportEmail]); _ -> io:format("Admins already exist. Skipping creation.~n") 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.