diff --git a/_build/dev/lib/jose/.mix/compile.erlang b/_build/dev/lib/jose/.mix/compile.erlang index 9d8b8db..9b9eeba 100644 Binary files a/_build/dev/lib/jose/.mix/compile.erlang and b/_build/dev/lib/jose/.mix/compile.erlang differ diff --git a/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex b/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex index 22ee133..69ffd9e 100644 --- a/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex +++ b/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex @@ -26,6 +26,19 @@ defmodule CentralcloudCore.HostBill do call("getInvoices", %{clientid: client_id}) end + @doc "Create a ticket via HostBill API." + @spec add_ticket(integer(), String.t(), String.t(), integer(), String.t()) :: + {:ok, map()} | {:error, term()} + def add_ticket(client_id, subject, message, department_id \\ 1, priority \\ "High") do + call("addTicket", %{ + clientid: client_id, + subject: subject, + message: message, + departmentid: department_id, + priority: priority + }) + end + # --------------------------------------------------------------------------- # Private diff --git a/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex b/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex new file mode 100644 index 0000000..ea073b7 --- /dev/null +++ b/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex @@ -0,0 +1,237 @@ +defmodule CentralcloudMy.EmergencyLive do + @moduledoc """ + Customer emergency page at /emergency. + + Allows authenticated customers to raise an emergency ticket that immediately + creates an incident in ops and pages the on-call engineer. + """ + + use Phoenix.LiveView + + alias CentralcloudCore.HostBill + + require Logger + + def render(assigns) do + ~H""" +
+
+

🚨 Emergency Support

+

+ This will immediately alert our on-call engineer. Only use for outages or security incidents. +

+
+ + <%= if @submitted do %> +
+

✓ Emergency reported

+

Our on-call engineer has been paged. Ticket reference: <%= @ticket_ref %>

+

+ You will receive updates via email and push notification. +

+
+ <% else %> +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + <%= if @error do %> +
+ <%= @error %> +
+ <% end %> +
+ <% end %> +
+ """ + end + + def mount(_params, _session, socket) do + services = fetch_services(socket.assigns[:current_user]) + + {:ok, + assign(socket, + subject: "", + body: "", + service_id: "", + services: services, + valid: false, + submitted: false, + ticket_ref: nil, + error: nil + )} + end + + def handle_event("validate", params, socket) do + subject = params["subject"] || "" + body = params["body"] || "" + confirm = params["confirm"] == "true" + + valid = + String.trim(subject) != "" and + String.trim(body) != "" and + confirm + + {:noreply, + assign(socket, + subject: subject, + body: body, + service_id: params["service_id"] || "", + valid: valid, + error: nil + )} + end + + def handle_event("submit", params, socket) do + user = socket.assigns[:current_user] + subject = String.trim(params["subject"] || "") + body = String.trim(params["body"] || "") + service_id = params["service_id"] || "" + + if subject == "" or body == "" do + {:noreply, assign(socket, error: "Subject and description are required.")} + else + client_id = user[:hostbill_client_id] || user["hostbill_client_id"] + + # Create emergency ticket in HostBill + ticket_result = + HostBill.call("addTicket", %{ + clientid: client_id, + subject: "[EMERGENCY] #{subject}", + message: body, + departmentid: 1, + priority: "High" + }) + + case ticket_result do + {:ok, %{"ticketid" => ticket_id}} -> + # Notify ops backend via webhook + notify_ops_emergency(ticket_id, subject, body, client_id, service_id) + + {:noreply, + assign(socket, + submitted: true, + ticket_ref: ticket_id, + error: nil + )} + + {:ok, _} -> + {:noreply, assign(socket, error: "Ticket created but response format unexpected.")} + + {:error, reason} -> + Logger.error("[EmergencyLive] Failed to create HostBill ticket: #{inspect(reason)}") + {:noreply, assign(socket, error: "Failed to create ticket. Please try again or call directly.")} + end + end + end + + defp fetch_services(nil), do: [] + + defp fetch_services(user) do + client_id = user[:hostbill_client_id] || user["hostbill_client_id"] + + if client_id do + case HostBill.get_client_services(client_id) do + {:ok, services} when is_list(services) -> + Enum.map(services, fn s -> + %{id: s["id"] || s["service_id"] || "", name: s["name"] || s["domain"] || "Service"} + end) + |> Enum.reject(&(&1.id == "")) + + _ -> + [] + end + else + [] + end + end + + defp notify_ops_emergency(ticket_id, subject, body, client_id, service_id) do + ops_url = System.get_env("OPS_URL", "https://ops.centralcloud.com") + + payload = %{ + event: "emergency_ticket", + details: %{ + ticket_id: ticket_id, + subject: subject, + body: body, + client_id: client_id, + service_id: service_id, + source: "my.centralcloud.com" + } + } + + # Fire webhook to ops backend + Task.start(fn -> + secret = System.get_env("HOSTBILL_WEBHOOK_SECRET", "") + raw_body = Jason.encode!(payload) + signature = :crypto.mac(:hmac, :sha256, secret, raw_body) |> Base.encode16(case: :lower) + + Req.post("#{ops_url}/api/hostbill/events", + body: raw_body, + headers: [ + {"content-type", "application/json"}, + {"x-centralcloud-signature", signature} + ], + retry: false + ) + end) + end +end diff --git a/apps/centralcloud_my/lib/centralcloud_my/router.ex b/apps/centralcloud_my/lib/centralcloud_my/router.ex index 9967dd0..f61b652 100644 --- a/apps/centralcloud_my/lib/centralcloud_my/router.ex +++ b/apps/centralcloud_my/lib/centralcloud_my/router.ex @@ -47,5 +47,6 @@ defmodule CentralcloudMy.Router do live "/billing", BillingLive, :index live "/security", SecurityLive, :index live "/support", SupportLive, :index + live "/emergency", EmergencyLive, :index end end