Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/phoenix live dashboard #461

Open
wants to merge 11 commits into
base: feature/phoenix-1.5
Choose a base branch
from
4 changes: 3 additions & 1 deletion config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ config :ret, RetWeb.Endpoint,
url: [host: "localhost"],
secret_key_base: "txlMOtlaY5x3crvOCko4uV5PM29ul3zGo1oBGNO3cDXx+7GHLKqt0gR9qzgThxb5",
render_errors: [view: RetWeb.ErrorView, accepts: ~w(html json)],
pubsub_server: Ret.PubSub
pubsub_server: Ret.PubSub,
# TODO: Load salt securely for production environment
live_view: [signing_salt: "p1pqfyVx4YPcjkZYz5PiQJDm0XlMWYk7"]

# Configures Elixir's Logger
config :logger, :console,
Expand Down
1 change: 1 addition & 0 deletions lib/ret/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ defmodule Ret.Application do

# Start the Ecto repository
supervisor(Ret.Repo, []),
RetWeb.Telemetry,
supervisor(RetWeb.Endpoint, []),
supervisor(RetWeb.Presence, []),

Expand Down
12 changes: 12 additions & 0 deletions lib/ret_web/auth_pipeline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ defmodule RetWeb.Guardian.AuthPipeline do
error_handler: RetWeb.Guardian.AuthErrorHandler

plug(Guardian.Plug.VerifyHeader, realm: "Bearer")
# TODO: Move configuration elsewhere
plug Plug.Session,
store: :cookie,
key: "_ret_session",
# TODO: Provide real salts (safely)
encryption_salt: "8XD1Tqa223TZ/1pErZGaKDWLbnEFfdo/",
signing_salt: "ZXAeUzIQJdzKT5WmxUQpROOL7eqK1FsX",
key_length: 64,
log: :debug

plug(RetWeb.Plugs.DecryptAuthCookieIntoSession)
plug(Guardian.Plug.VerifySession)
plug(Guardian.Plug.EnsureAuthenticated)
plug(Guardian.Plug.LoadResource)
end
29 changes: 29 additions & 0 deletions lib/ret_web/controllers/api/v1/account_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,33 @@ defmodule RetWeb.Api.V1.AccountController do
{:ok, {200, Phoenix.View.render(AccountView, "create.json", account: account, email: email)}}
end
end

def set_cookie(conn, _params) do
conn
|> set_account_cookie(%{
value: Ret.Guardian.Plug.current_token(conn),
max_age: 60 * 60 * 24
})
|> Plug.Conn.send_resp(200, "")
end

def expire_cookie(conn, _params) do
conn
|> set_account_cookie(%{value: "", max_age: 60})
|> Plug.Conn.send_resp(200, "")
end

defp set_account_cookie(conn, %{value: value, max_age: max_age}) do
key = Guardian.Plug.Keys.token_key("default") |> Atom.to_string()

opts = [
encrypt: true,
max_age: max_age,
http_only: true,
secure: true
]

conn
|> Plug.Conn.put_resp_cookie(key, value, opts)
end
end
6 changes: 6 additions & 0 deletions lib/ret_web/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ defmodule RetWeb.Endpoint do
use Sentry.Phoenix.Endpoint
use Absinthe.Phoenix.Endpoint

socket "/live", Phoenix.LiveView.Socket

socket("/socket", RetWeb.SessionSocket, websocket: [check_origin: {RetWeb.Endpoint, :allowed_origin?, []}])

def get_cors_origins, do: Application.get_env(:ret, RetWeb.Endpoint)[:allowed_origins] |> String.split(",")
Expand All @@ -24,6 +26,10 @@ defmodule RetWeb.Endpoint do
plug(Phoenix.CodeReloader)
end

plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"

plug(Plug.RequestId)
plug(Plug.Logger)

Expand Down
12 changes: 5 additions & 7 deletions lib/ret_web/plugs/add_csp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,11 @@ defmodule RetWeb.Plugs.AddCSP do
}:#{janus_port} #{default_janus_csp_rule}"
end

"default-src 'none'; manifest-src #{custom_rules[:manifest_src]} 'self'; script-src #{custom_rules[:script_src]} #{
storage_url
} #{assets_url} 'self' 'unsafe-eval' 'sha256-ViVvpb0oYlPAp7R8ZLxlNI6rsf7E7oz8l1SgCIXgMvM=' 'sha256-hsbRcgUBASABDq7qVGVTpbnWq/ns7B+ToTctZFJXYi8=' 'sha256-MIpWPgYj31kCgSUFc0UwHGQrV87W6N5ozotqfxxQG0w=' 'sha256-buF6N8Z4p2PuaaeRUjm7mxBpPNf4XlCT9Fep83YabbM=' 'sha256-/S6PM16MxkmUT7zJN2lkEKFgvXR7yL4Z8PCrRrFu4Q8=' https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.js 'sha256-foB3G7vO68Ot8wctsG3OKBQ84ADKVinlnTg9/s93Ycs=' 'sha256-g0j42v3Wo/ohUAMR/t0EuObDSEkx1rZ3lv45fUaNmYs=' https://www.google-analytics.com https://ssl.google-analytics.com #{
storage_url
} #{assets_url} https://aframe.io https://www.youtube.com https://s.ytimg.com; child-src #{custom_rules[:child_src]} 'self' blob:; worker-src #{
custom_rules[:worker_src]
} #{storage_url} #{assets_url} 'self' blob:; font-src #{custom_rules[:font_src]} 'self' https://fonts.googleapis.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdn.aframe.io #{
"default-src 'none'; manifest-src #{custom_rules[:manifest_src]} 'self'; script-src * 'unsafe-inline' 'unsafe-eval'; child-src #{
custom_rules[:child_src]
} 'self' blob:; worker-src #{custom_rules[:worker_src]} #{storage_url} #{assets_url} 'self' blob:; font-src #{
custom_rules[:font_src]
} 'self' https://fonts.googleapis.com https://cdn.jsdelivr.net https://fonts.gstatic.com https://cdn.aframe.io #{
storage_url
} #{assets_url} #{cors_proxy_url}; style-src #{custom_rules[:style_src]} 'self' https://fonts.googleapis.com https://cdn.jsdelivr.net #{
cors_proxy_url
Expand Down
14 changes: 14 additions & 0 deletions lib/ret_web/plugs/decrypt_auth_cookie_into_session.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
defmodule RetWeb.Plugs.DecryptAuthCookieIntoSession do
def init([]), do: []

def call(conn, []) do
conn = Plug.Conn.fetch_cookies(conn, encrypted: "guardian_default_token")
# Plug.Conn.fetch_cookies decrypts into conn.cookies
# But Guardian.Plug.VerifyCookie reads from conn.req_cookies
# so instead we put token into a session and use
# Guardian.Plug.VerifySession
conn = Plug.Conn.fetch_session(conn)
conn = Plug.Conn.put_session(conn, "guardian_default_token", conn.cookies["guardian_default_token"] || nil)
conn
end
end
20 changes: 20 additions & 0 deletions lib/ret_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule RetWeb.Router do
use RetWeb, :router
use Plug.ErrorHandler
use Sentry.Plug
import Phoenix.LiveDashboard.Router

pipeline :secure_headers do
plug(:put_secure_browser_headers)
Expand Down Expand Up @@ -88,6 +89,25 @@ defmodule RetWeb.Router do
forward("/", RetWeb.Plugs.ItaProxy)
end

scope "/", RetWeb do
pipe_through(
[:secure_headers, :parsed_body, :browser] ++
if(Mix.env() == :prod, do: [:ssl_only, :canonicalize_domain], else: [])
)

post("/api/v1/accounts/expire_cookie", Api.V1.AccountController, :expire_cookie)

scope "/" do
pipe_through([:admin_required])

# TODO: investigate whether we have pg_stat_statements installed for postgres
# https://www.postgresql.org/docs/current/pgstatstatements.html
# https://hexdocs.pm/phoenix_live_dashboard/ecto_stats.html#install-custom-extensions
live_dashboard "/telemetry", metrics: RetWeb.Telemetry, ecto_repos: [Ret.Repo]
post("/api/v1/accounts/set_cookie", Api.V1.AccountController, :set_cookie)
end
end

scope "/api", RetWeb do
pipe_through(
[:secure_headers, :parsed_body, :api] ++ if(Mix.env() == :prod, do: [:ssl_only, :canonicalize_domain], else: [])
Expand Down
68 changes: 68 additions & 0 deletions lib/ret_web/telemetry.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# TODO: Add metrics from various telemetry-enabled libraries (e.g. absinthe)
# TODO: Add storage and enable history https://hexdocs.pm/phoenix_live_dashboard/metrics_history.html
defmodule RetWeb.Telemetry do
use Supervisor
import Telemetry.Metrics

def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end

@impl true
def init(_arg) do
children = [
# Telemetry poller will execute the given period measurements
# every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics
{:telemetry_poller, measurements: periodic_measurements(), period: 10_000}
# Add reporters as children of your supervision tree.
# {Telemetry.Metrics.ConsoleReporter, metrics: metrics()}
]

Supervisor.init(children, strategy: :one_for_one)
end

def metrics do
[
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}
),
summary("phoenix.router_dispatch.stop.duration",
tags: [:route],
unit: {:native, :millisecond}
),

# Database Time Metrics
summary("ret.repo.query.total_time", unit: {:native, :millisecond}),
summary("ret.repo.query.decode_time", unit: {:native, :millisecond}),
summary("ret.repo.query.query_time", unit: {:native, :millisecond}),
summary("ret.repo.query.queue_time", unit: {:native, :millisecond}),
summary("ret.repo.query.idle_time", unit: {:native, :millisecond}),

# VM Metrics
summary("vm.memory.total", unit: {:byte, :kilobyte}),
summary("vm.total_run_queue_lengths.total"),
summary("vm.total_run_queue_lengths.cpu"),
summary("vm.total_run_queue_lengths.io"),

# Absinthe
# summary("absinthe.execute.operation.start"),
# summary("absinthe.execute.operation.stop"),
# summary("absinthe.subscription.publish.start"),
# summary("absinthe.subscription.publish.stop"),
# summary("absinthe.resolve.field.start"),
# summary("absinthe.resolve.field.stop"),
# summary("absinthe.middleware.batch.start"),
# summary("absinthe.middleware.batch.stop"),
# TODO: Learn how these metrics work
summary("absinthe.execute.operation.stop.duration"),
summary("absinthe.subscription.publish.stop.duration"),
summary("absinthe.resolve.field.stop.duration"),
summary("absinthe.middleware.batch.stop.duration")
]
end

defp periodic_measurements do
[]
end
end
13 changes: 8 additions & 5 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ defmodule Ret.Mixfile do
def application do
[
mod: {Ret.Application, []},
extra_applications: [:runtime_tools, :canada]
extra_applications: [:runtime_tools, :canada, :os_mon]
]
end

Expand All @@ -38,16 +38,16 @@ defmodule Ret.Mixfile do
{:phoenix_pubsub, "~> 2.0"},
{:phoenix_ecto, "~> 4.0"},
{:plug, "~> 1.7"},
# Avoid 3.4.0 for now bc https://github.com/elixir-ecto/ecto/issues/3246
{:ecto, "~> 3.3.0"},
{:ecto_sql, "~> 3.3.0"},
{:ecto, "~> 3.5.0"},
{:ecto_sql, "~> 3.5.0"},
{:absinthe, "~> 1.5.0"},
{:dataloader, "~> 1.0.0"},
{:absinthe_plug, "~> 1.5.0"},
{:absinthe_phoenix, "~> 2.0.0"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.13"},
{:phoenix_live_reload, "~> 1.2", only: :dev},
{:phoenix_live_dashboard, "~> 0.1"},
{:gettext, "~> 0.17"},
{:plug_cowboy, "~> 2.1"},
{:distillery, "~> 2.0"},
Expand Down Expand Up @@ -87,7 +87,10 @@ defmodule Ret.Mixfile do
{:ex_rated, "~> 1.3.3"},
{:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
{:ex_json_schema, "~> 0.7.3"},
{:observer_cli, "~> 1.5"}
{:observer_cli, "~> 1.5"},
{:telemetry_poller, "~> 0.4"},
{:telemetry_metrics, "~> 0.4"},
{:ecto_psql_extras, "~> 0.2"}
]
end

Expand Down
Loading