diff --git a/elixir/apps/domain/lib/domain/auth.ex b/elixir/apps/domain/lib/domain/auth.ex index 5227419fa..46c66efc6 100644 --- a/elixir/apps/domain/lib/domain/auth.ex +++ b/elixir/apps/domain/lib/domain/auth.ex @@ -142,11 +142,9 @@ defmodule Domain.Auth do end def list_providers_pending_sync_by_adapter(adapter) do - datetime_filter = DateTime.utc_now() |> DateTime.add(-10, :minute) - Provider.Query.by_adapter(adapter) |> Provider.Query.by_provisioner(:custom) - |> Provider.Query.last_synced_at({:lt, datetime_filter}) + |> Provider.Query.only_ready_to_be_synced() |> Provider.Query.not_disabled() |> Repo.list() end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/api_client.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/api_client.ex index 23ef3ef68..aa5bd0350 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/api_client.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/api_client.ex @@ -126,10 +126,23 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.APIClient do {:ok, list} <- Map.fetch(json_response, key) do {:ok, list, json_response["nextPageToken"]} else - {:ok, %Finch.Response{status: status}} when status in 500..599 -> {:error, :retry_later} - {:ok, %Finch.Response{body: response, status: status}} -> {:error, {status, response}} - :error -> {:ok, [], nil} - other -> other + {:ok, %Finch.Response{status: status}} when status in 500..599 -> + {:error, :retry_later} + + {:ok, %Finch.Response{body: response, status: status}} -> + case Jason.decode(response) do + {:ok, json_response} -> + {:error, {status, json_response}} + + _error -> + {:error, {status, response}} + end + + :error -> + {:ok, [], nil} + + other -> + other end end end diff --git a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex index 222ec3803..8397c853e 100644 --- a/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex +++ b/elixir/apps/domain/lib/domain/auth/adapters/google_workspace/jobs.ex @@ -100,7 +100,7 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do |> Ecto.Multi.append(Actors.sync_provider_groups_multi(provider, actor_groups_attrs)) |> Actors.sync_provider_memberships_multi(provider, tuples) |> Ecto.Multi.update(:save_last_updated_at, fn _effects_so_far -> - Ecto.Changeset.change(provider, last_synced_at: DateTime.utc_now()) + Auth.Provider.Changeset.sync_finished(provider) end) |> Domain.Repo.transaction() |> case do @@ -152,8 +152,25 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do ) end else + {:error, {status, %{"error" => %{"message" => message}}}} -> + provider = + Auth.Provider.Changeset.sync_failed(provider, message) + |> Domain.Repo.update!() + + log_sync_error(provider, "Google API returned #{status}: #{message}") + + {:error, :retry_later} -> + message = "Google API is temporarily unavailable" + + provider = + Auth.Provider.Changeset.sync_failed(provider, message) + |> Domain.Repo.update!() + + log_sync_error(provider, message) + {:error, reason} -> Logger.error("Failed syncing provider", + account_id: provider.account_id, provider_id: provider.id, reason: inspect(reason) ) @@ -163,6 +180,20 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.Jobs do end end + defp log_sync_error(provider, message) do + metadata = [ + account_id: provider.account_id, + provider_id: provider.id, + reason: message + ] + + if provider.last_syncs_failed >= 3 do + Logger.warning("Failed syncing provider", metadata) + else + Logger.info("Failed syncing provider", metadata) + end + end + defp list_membership_tuples(access_token, groups) do Enum.reduce_while(groups, {:ok, []}, fn group, {:ok, tuples} -> case GoogleWorkspace.APIClient.list_group_members(access_token, group["id"]) do diff --git a/elixir/apps/domain/lib/domain/auth/provider.ex b/elixir/apps/domain/lib/domain/auth/provider.ex index d19452a3c..a752730a7 100644 --- a/elixir/apps/domain/lib/domain/auth/provider.ex +++ b/elixir/apps/domain/lib/domain/auth/provider.ex @@ -17,7 +17,10 @@ defmodule Domain.Auth.Provider do field :created_by, Ecto.Enum, values: ~w[system identity]a belongs_to :created_by_identity, Domain.Auth.Identity + field :last_syncs_failed, :integer + field :last_sync_error, :string field :last_synced_at, :utc_datetime_usec + field :disabled_at, :utc_datetime_usec field :deleted_at, :utc_datetime_usec timestamps() diff --git a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex index 6fa31d83c..ae1ade9af 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/changeset.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/changeset.ex @@ -4,7 +4,7 @@ defmodule Domain.Auth.Provider.Changeset do alias Domain.Auth.{Subject, Provider} @create_fields ~w[id name adapter provisioner adapter_config adapter_state disabled_at]a - @update_fields ~w[name adapter_config adapter_state provisioner disabled_at deleted_at]a + @update_fields ~w[name adapter_config last_syncs_failed last_sync_error adapter_state provisioner disabled_at deleted_at]a @required_fields ~w[name adapter adapter_config provisioner]a def create(account, attrs, %Subject{} = subject) do @@ -28,6 +28,24 @@ defmodule Domain.Auth.Provider.Changeset do |> changeset() end + def sync_finished(%Provider{} = provider) do + provider + |> change() + |> put_change(:last_synced_at, DateTime.utc_now()) + |> put_change(:last_sync_error, nil) + |> put_change(:last_syncs_failed, 0) + end + + def sync_failed(%Provider{} = provider, error) do + last_syncs_failed = provider.last_syncs_failed || 0 + + provider + |> change() + |> put_change(:last_synced_at, nil) + |> put_change(:last_sync_error, error) + |> put_change(:last_syncs_failed, last_syncs_failed + 1) + end + defp changeset(changeset) do changeset |> validate_length(:name, min: 1, max: 255) diff --git a/elixir/apps/domain/lib/domain/auth/provider/query.ex b/elixir/apps/domain/lib/domain/auth/provider/query.ex index f1735a01e..99b26faa3 100644 --- a/elixir/apps/domain/lib/domain/auth/provider/query.ex +++ b/elixir/apps/domain/lib/domain/auth/provider/query.ex @@ -38,6 +38,19 @@ defmodule Domain.Auth.Provider.Query do ) end + def only_ready_to_be_synced(queryable \\ not_deleted()) do + where( + queryable, + [provider: provider], + is_nil(provider.last_synced_at) or + fragment( + "? + LEAST((interval '10 minute' * (COALESCE(?, 0) ^ 2 + 1)), interval '4 hours') < NOW()", + provider.last_synced_at, + provider.last_syncs_failed + ) + ) + end + def by_non_empty_refresh_token(queryable \\ not_deleted()) do where( queryable, @@ -66,6 +79,10 @@ defmodule Domain.Auth.Provider.Query do where(queryable, [provider: provider], is_nil(provider.disabled_at)) end + def not_exceeded_attempts(queryable \\ not_deleted()) do + where(queryable, [provider: provider], provider.last_syncs_failed <= 10) + end + def lock(queryable \\ not_deleted()) do lock(queryable, "FOR UPDATE") end diff --git a/elixir/apps/domain/lib/domain/clients.ex b/elixir/apps/domain/lib/domain/clients.ex index cf145f1bf..ec035eb70 100644 --- a/elixir/apps/domain/lib/domain/clients.ex +++ b/elixir/apps/domain/lib/domain/clients.ex @@ -1,7 +1,7 @@ defmodule Domain.Clients do use Supervisor alias Domain.{Repo, Auth, Validator} - alias Domain.Actors + alias Domain.{Accounts, Actors} alias Domain.Clients.{Client, Authorizer, Presence} require Ecto.Query @@ -127,13 +127,15 @@ defmodule Domain.Clients do # TODO: this is ugly! defp preload_online_status(client) do - connected_clients = Presence.list("clients:#{client.id}") - %{client | online?: Map.has_key?(connected_clients, client.id)} + case Presence.get_by_key("clients:#{client.account_id}", client.id) do + [] -> %{client | online?: false} + %{metas: [_ | _]} -> %{client | online?: true} + end end - defp preload_online_statuses([]), do: [] + def preload_online_statuses([]), do: [] - defp preload_online_statuses([client | _] = clients) do + def preload_online_statuses([client | _] = clients) do connected_clients = Presence.list("clients:#{client.account_id}") Enum.map(clients, fn client -> @@ -233,6 +235,18 @@ defmodule Domain.Clients do :ok end + def subscribe_for_clients_presence_in_account(%Accounts.Account{} = account) do + subscribe_for_clients_presence_in_account(account.id) + end + + def subscribe_for_clients_presence_in_account(account_id) do + Phoenix.PubSub.subscribe(Domain.PubSub, "clients:#{account_id}") + end + + def subscribe_for_clients_presence_for_actor(%Actors.Actor{} = actor) do + Phoenix.PubSub.subscribe(Domain.PubSub, "actor_clients:#{actor.id}") + end + def fetch_client_config!(%Client{} = client) do %{ clients_upstream_dns: upstream_dns diff --git a/elixir/apps/domain/lib/domain/cluster/google_compute_labels_strategy.ex b/elixir/apps/domain/lib/domain/cluster/google_compute_labels_strategy.ex index 27ce94840..9a4bfef55 100644 --- a/elixir/apps/domain/lib/domain/cluster/google_compute_labels_strategy.ex +++ b/elixir/apps/domain/lib/domain/cluster/google_compute_labels_strategy.ex @@ -123,13 +123,17 @@ defmodule Domain.Cluster.GoogleComputeLabelsStrategy do end {:error, reason} -> - Logger.error("Can not fetch list of nodes or access token: #{inspect(reason)}", - module: __MODULE__ - ) - if remaining_retry_count == 0 do + Logger.error("Can not fetch list of nodes or access token: #{inspect(reason)}", + module: __MODULE__ + ) + {:error, reason} else + Logger.warning("Can not fetch list of nodes or access token: #{inspect(reason)}", + module: __MODULE__ + ) + backoff_interval = Keyword.get(state.config, :backoff_interval, 1_000) :timer.sleep(backoff_interval) fetch_nodes(state, remaining_retry_count - 1) diff --git a/elixir/apps/domain/priv/repo/migrations/20231218175356_add_auth_providers_last_sync_error.exs b/elixir/apps/domain/priv/repo/migrations/20231218175356_add_auth_providers_last_sync_error.exs new file mode 100644 index 000000000..5e545b0e3 --- /dev/null +++ b/elixir/apps/domain/priv/repo/migrations/20231218175356_add_auth_providers_last_sync_error.exs @@ -0,0 +1,10 @@ +defmodule Domain.Repo.Migrations.AddAuthProvidersLastSyncError do + use Ecto.Migration + + def change do + alter table(:auth_providers) do + add(:last_syncs_failed, :integer, default: 0) + add(:last_sync_error, :text) + end + end +end diff --git a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs index d552ea433..073f61546 100644 --- a/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs +++ b/elixir/apps/domain/test/domain/auth/adapters/google_workspace/jobs_test.exs @@ -466,5 +466,75 @@ defmodule Domain.Auth.Adapters.GoogleWorkspace.JobsTest do assert org_unit.id in membership_group_ids assert deleted_group.id not in membership_group_ids end + + test "persists the sync error on the provider", %{provider: provider} do + bypass = Bypass.open() + GoogleWorkspaceDirectory.override_endpoint_url("http://localhost:#{bypass.port}/") + + error_message = + "Admin SDK API has not been used in project XXXX before or it is disabled. " <> + "Enable it by visiting https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=XXXX " <> + "then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry." + + response = + %{ + "error" => %{ + "code" => 403, + "message" => error_message, + "errors" => [ + %{ + "message" => error_message, + "domain" => "usageLimits", + "reason" => "accessNotConfigured", + "extendedHelp" => "https://console.developers.google.com" + } + ], + "status" => "PERMISSION_DENIED", + "details" => [ + %{ + "@type" => "type.googleapis.com/google.rpc.Help", + "links" => [ + %{ + "description" => "Google developers console API activation", + "url" => + "https://console.developers.google.com/apis/api/admin.googleapis.com/overview?project=100421656358" + } + ] + }, + %{ + "@type" => "type.googleapis.com/google.rpc.ErrorInfo", + "reason" => "SERVICE_DISABLED", + "domain" => "googleapis.com", + "metadata" => %{ + "service" => "admin.googleapis.com", + "consumer" => "projects/100421656358" + } + } + ] + } + } + + Bypass.expect_once(bypass, "GET", "/admin/directory/v1/users", fn conn -> + Plug.Conn.send_resp(conn, 403, Jason.encode!(response)) + end) + + assert sync_directory(%{}) == :ok + + assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id) + refute updated_provider.last_synced_at + assert updated_provider.last_syncs_failed == 1 + assert updated_provider.last_sync_error == error_message + + Bypass.expect_once(bypass, "GET", "/admin/directory/v1/users", fn conn -> + Plug.Conn.send_resp(conn, 500, "") + end) + + assert sync_directory(%{}) == :ok + + assert updated_provider = Repo.get(Domain.Auth.Provider, provider.id) + refute updated_provider.last_synced_at + assert updated_provider.last_syncs_failed == 2 + assert updated_provider.last_sync_error == "Google API is temporarily unavailable" + end end end diff --git a/elixir/apps/domain/test/domain/auth_test.exs b/elixir/apps/domain/test/domain/auth_test.exs index 0d334e60b..2c12e5940 100644 --- a/elixir/apps/domain/test/domain/auth_test.exs +++ b/elixir/apps/domain/test/domain/auth_test.exs @@ -362,6 +362,31 @@ defmodule Domain.AuthTest do assert Enum.map(providers, & &1.id) |> Enum.sort() == Enum.sort([provider1.id, provider2.id]) end + + test "uses 1/2 regular timeout backoff for failed attempts" do + {provider, _bypass} = Fixtures.Auth.start_and_create_google_workspace_provider() + # backoff: 10 minutes * (1 + 3 ^ 2) = 100 minutes + provider = Domain.Fixture.update!(provider, %{last_sync_error: "foo", last_syncs_failed: 3}) + + ninety_nine_minute_ago = DateTime.utc_now() |> DateTime.add(-99, :minute) + Domain.Fixture.update!(provider, %{last_synced_at: ninety_nine_minute_ago}) + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + + one_hundred_one_minute_ago = DateTime.utc_now() |> DateTime.add(-101, :minute) + Domain.Fixture.update!(provider, %{last_synced_at: one_hundred_one_minute_ago}) + assert {:ok, [_provider]} = list_providers_pending_sync_by_adapter(:google_workspace) + + # max backoff: 4 hours + provider = Domain.Fixture.update!(provider, %{last_syncs_failed: 300}) + + three_hours_fifty_nine_minutes_ago = DateTime.utc_now() |> DateTime.add(-239, :minute) + Domain.Fixture.update!(provider, %{last_synced_at: three_hours_fifty_nine_minutes_ago}) + assert list_providers_pending_sync_by_adapter(:google_workspace) == {:ok, []} + + four_hours_one_minute_ago = DateTime.utc_now() |> DateTime.add(-241, :minute) + Domain.Fixture.update!(provider, %{last_synced_at: four_hours_one_minute_ago}) + assert {:ok, [_provider]} = list_providers_pending_sync_by_adapter(:google_workspace) + end end describe "new_provider/2" do diff --git a/elixir/apps/domain/test/domain/clients_test.exs b/elixir/apps/domain/test/domain/clients_test.exs index d329285d5..be94943d0 100644 --- a/elixir/apps/domain/test/domain/clients_test.exs +++ b/elixir/apps/domain/test/domain/clients_test.exs @@ -71,6 +71,17 @@ defmodule Domain.ClientsTest do assert fetch_client_by_id(client.id, subject) == {:ok, client} end + test "preloads online status", %{unprivileged_actor: actor, unprivileged_subject: subject} do + client = Fixtures.Clients.create_client(actor: actor) + + assert {:ok, client} = fetch_client_by_id(client.id, subject) + assert client.online? == false + + assert connect_client(client) == :ok + assert {:ok, client} = fetch_client_by_id(client.id, subject) + assert client.online? == true + end + test "returns client that belongs to another actor with manage permission", %{ account: account, unprivileged_subject: subject @@ -182,6 +193,17 @@ defmodule Domain.ClientsTest do assert length(clients) == 2 end + test "preloads online status", %{unprivileged_actor: actor, unprivileged_subject: subject} do + Fixtures.Clients.create_client(actor: actor) + + assert {:ok, [client]} = list_clients(subject) + assert client.online? == false + + assert connect_client(client) == :ok + assert {:ok, [client]} = list_clients(subject) + assert client.online? == true + end + test "returns error when subject has no permission to manage clients", %{ unprivileged_subject: subject } do diff --git a/elixir/apps/domain/test/support/fixtures/auth.ex b/elixir/apps/domain/test/support/fixtures/auth.ex index 157ac65d0..a26caae66 100644 --- a/elixir/apps/domain/test/support/fixtures/auth.ex +++ b/elixir/apps/domain/test/support/fixtures/auth.ex @@ -198,6 +198,18 @@ defmodule Domain.Fixtures.Auth do update!(provider, deleted_at: DateTime.utc_now()) end + def fail_provider_sync(provider) do + update!(provider, last_sync_error: "Message from fixture", last_syncs_failed: 3) + end + + def finish_provider_sync(provider) do + update!(provider, + last_synced_at: DateTime.utc_now(), + last_sync_error: nil, + last_syncs_failed: 0 + ) + end + def identity_attrs(attrs \\ %{}) do Enum.into(attrs, %{ provider_virtual_state: %{} diff --git a/elixir/apps/web/assets/js/hooks.js b/elixir/apps/web/assets/js/hooks.js index 77fafc07b..6d10815d9 100644 --- a/elixir/apps/web/assets/js/hooks.js +++ b/elixir/apps/web/assets/js/hooks.js @@ -1,4 +1,3 @@ -import StatusPage from "../vendor/status_page"; import { initTabs } from "flowbite"; let Hooks = {}; @@ -62,38 +61,4 @@ Hooks.Copy = { }, }; -// Update status indicator when sidebar is mounted or updated -let statusIndicatorClassNames = { - none: "bg-green-100 text-green-800", - minor: "bg-yellow-100 text-yellow-800", - major: "bg-orange-100 text-orange-800", - critical: "bg-red-100 text-red-800", -}; - -const statusUpdater = function () { - const self = this; - const sp = new StatusPage.page({ page: "firezone" }); - - sp.summary({ - success: function (data) { - self.el.innerHTML = ` - - ${data.status.description} - - `; - }, - error: function (data) { - console.error("An error occurred while fetching status page data"); - self.el.innerHTML = `Unable to fetch status`; - }, - }); -}; - -Hooks.StatusPage = { - mounted: statusUpdater, - updated: statusUpdater, -}; - export default Hooks; diff --git a/elixir/apps/web/assets/vendor/status_page.js b/elixir/apps/web/assets/vendor/status_page.js deleted file mode 100644 index f32088a94..000000000 --- a/elixir/apps/web/assets/vendor/status_page.js +++ /dev/null @@ -1,184 +0,0 @@ -// StatusPage API Wrapper. Vendored and reviewed by @jamilbk from https://cdn.statuspage.io/se-v2.js -;(StatusPage = "undefined" == typeof StatusPage ? {} : StatusPage), - (StatusPage.page = function (e) { - if (!(e = e || {}).page) - throw new Error("A pageId is required to initialize.") - ;(this.apiKey = e.apiKey || null), - (this.error = e.error || this.error), - (this.format = e.format || "json"), - (this.pageId = e.page), - (this.version = e.version || "v2"), - (this.secure = !("secure" in e) || e.secure), - (this.protocol = this.secure ? "https" : "http"), - (this.host = e.host || "statuspage.io"), - (this.host_with_port_and_protocol = e.test - ? "" - : this.protocol + "://" + this.pageId + "." + this.host) - }), - (StatusPage.page.prototype.serialize = function (e, t) { - var s = [], - r = { sms: "email_sms", webhook: "endpoint" } - for (var o in e) - if ("to_sentence" !== o) { - var i = o - o = o in r ? r[o] : o - var a = t ? t + "[" + o + "]" : o, - n = e[i] - s.push( - "object" == typeof n - ? this.serialize(n, a) - : encodeURIComponent(a) + "=" + encodeURIComponent(n) - ) - } - return s.join("&") - }), - (StatusPage.page.prototype.createStatusPageCORSRequest = function (e, t) { - var s = new XMLHttpRequest() - return ( - "withCredentials" in s - ? s.open(e, t, !0) - : "undefined" != typeof XDomainRequest - ? (s = new XDomainRequest()).open(e, t) - : (s = null), - s - ) - }), - (StatusPage.page.prototype.executeRequestAndCallbackWithResponse = function ( - e - ) { - if (!e.path) throw new Error("A path is required to make a request") - var t = e.path, - s = e.method || "GET", - r = e.success || null, - o = e.error || this.error, - i = - this.host_with_port_and_protocol + - "/api/" + - this.version + - "/" + - t + - "." + - this.format, - a = this.createStatusPageCORSRequest(s, i) - if (a) - if ( - (this.apiKey && - (console.log( - "!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!" - ), - console.log( - "!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!" - ), - console.log( - "!!! API KEY IN USE - REMOVE BEFORE DEPLOYING TO PRODUCTION !!!" - ), - a.setRequestHeader("Authorization", "OAuth " + this.apiKey)), - (a.onload = function () { - var e = JSON.parse(a.responseText) - r && r(e) - }), - (a.onerror = o), - "POST" === s || "DELETE" === s) - ) { - var n = e.data || {} - a.setRequestHeader("Content-type", "application/x-www-form-urlencoded"), - a.send(this.serialize(n)) - } else a.send() - }), - (StatusPage.page.prototype.get = function (e, t) { - if (((t = t || {}), !e)) throw new Error("Path is required.") - if (!t.success) throw new Error("Success Callback is required.") - var s = t.success || {}, - r = t.error || {} - this.executeRequestAndCallbackWithResponse({ - path: e, - success: s, - error: r, - method: "GET", - }) - }), - (StatusPage.page.prototype.post = function (e, t) { - if (((t = t || {}), !e)) throw new Error("Path is required.") - var s = {} - if ("subscribers" === e) { - if (!t.subscriber) throw new Error("Subscriber is required to post.") - s.subscriber = t.subscriber - } else { - if (!t.data) throw new Error("Data is required to post.") - s = t.data - } - var r = t.success || {}, - o = t.error || {} - this.executeRequestAndCallbackWithResponse({ - data: s, - path: e, - success: r, - error: o, - method: "POST", - }) - }), - (StatusPage.page.prototype["delete"] = function (e, t) { - if (((t = t || {}), !e)) throw new Error("Path is required.") - if (!t.subscriber) throw new Error("Data is required to delete.") - var s = {} - "subscribers" === e ? (s.subscriber = t.subscriber) : (s = t.data) - var r = t.success || {}, - o = t.error || {} - this.executeRequestAndCallbackWithResponse({ - data: s, - path: e, - success: r, - error: o, - method: "DELETE", - }) - }), - (StatusPage.page.prototype.error = function () { - console.log("There was an error with your request") - }), - (StatusPage.page.prototype.summary = function (e) { - this.get("summary", e) - }), - (StatusPage.page.prototype.status = function (e) { - this.get("status", e) - }), - (StatusPage.page.prototype.components = function (e) { - this.get("components", e) - }), - (StatusPage.page.prototype.incidents = function (e) { - switch (e.filter) { - case "unresolved": - this.get("incidents/unresolved", e) - break - case "resolved": - this.get("incidents/resolved", e) - break - default: - this.get("incidents", e) - } - }), - (StatusPage.page.prototype.scheduled_maintenances = function (e) { - switch (e.filter) { - case "active": - this.get("scheduled-maintenances/active", e) - break - case "upcoming": - this.get("scheduled-maintenances/upcoming", e) - break - default: - this.get("scheduled-maintenances", e) - } - }), - (StatusPage.page.prototype.subscribe = function (e) { - if (!e || !e.subscriber) throw new Error("A subscriber object is required.") - this.post("subscribers", e) - }), - (StatusPage.page.prototype.unsubscribe = function (e) { - if (!e || !e.subscriber) throw new Error("A subscriber object is required.") - if (!e.subscriber.id) - throw new Error( - "You must supply a subscriber.id in order to cancel a subscription." - ) - this["delete"]("subscribers", e) - }) - -export default StatusPage diff --git a/elixir/apps/web/lib/web/components/core_components.ex b/elixir/apps/web/lib/web/components/core_components.ex index c1ffd12a3..b0eab33d1 100644 --- a/elixir/apps/web/lib/web/components/core_components.ex +++ b/elixir/apps/web/lib/web/components/core_components.ex @@ -673,16 +673,6 @@ defmodule Web.CoreComponents do """ end - def status_page_widget(assigns) do - ~H""" -
+ IdP provider reported an error during the last sync: +
+