Compare commits

...

3 Commits

14 changed files with 199 additions and 23 deletions

View File

@ -13,3 +13,5 @@ import "../css/app.scss"
// import socket from "./socket"
//
import "phoenix_html"
import "./async_uploads"

View File

@ -0,0 +1,47 @@
document.addEventListener("DOMContentLoaded", () => {
const form = document.querySelector(".edit-upload-form");
if (!form) return;
const button = form.querySelector("button[type=submit]");
button.addEventListener("click", (event) => {
event.preventDefault();
button.setAttribute("disabled", "true");
const file = form.querySelector("input[type=file]").files[0];
if (!file) return;
const pageID = /^\/pages\/(\d+)\/edit$/.exec(window.location.pathname)[1];
const progress = form.querySelector("progress");
progress.style.visibility = "visible";
const fd = new FormData();
fd.append("_csrf_token", form.querySelector("input[type=hidden]").value);
fd.append("file", file);
// it's 2021 and fetch doesn't have progress support, smh
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (event) => {
const newVal = event.loaded / event.total;
if (newVal >= 1) {
progress.removeAttribute("value");
} else {
progress.value = newVal;
}
});
xhr.addEventListener("load", () => {
window.location.reload();
});
xhr.addEventListener("error", (event) => {
console.error(event);
progress.style.visibility = "hidden";
alert("Upload error");
});
xhr.addEventListener("abort", (event) => {
console.error(event);
progress.style.visibility = "hidden";
alert("Upload aborted");
});
xhr.open("POST", `/pages/${pageID}/uploads`);
xhr.send(fd);
});
});

View File

@ -7,7 +7,8 @@ defmodule Wiki.Accounts.User do
password: String.t() | nil,
hashed_password: String.t(),
confirmed_at: NaiveDateTime.t(),
content_encryption_key_salt: String.t()
content_encryption_key_salt: String.t(),
is_admin: boolean()
}
@derive {Inspect, except: [:password]}
@ -17,6 +18,7 @@ defmodule Wiki.Accounts.User do
field :hashed_password, :string
field :confirmed_at, :naive_datetime
field :content_encryption_key_salt, :binary
field :is_admin, :boolean, default: false
timestamps()
end

View File

@ -14,9 +14,11 @@ defmodule Wiki.Application do
# Start the PubSub system
{Phoenix.PubSub, name: Wiki.PubSub},
# Start the Endpoint (http/https)
WikiWeb.Endpoint
WikiWeb.Endpoint,
# Start a worker by calling: Wiki.Worker.start_link(arg)
# {Wiki.Worker, arg}
{Wiki.MetricStorage, WikiWeb.Telemetry.metrics()},
Wiki.MetricGenerator
]
# See https://hexdocs.pm/elixir/Supervisor.html

View File

@ -0,0 +1,34 @@
defmodule Wiki.MetricGenerator do
use GenServer
alias Wiki.Repo
alias Wiki.Content.{Page, PageLink, Upload}
def start_link(state) do
GenServer.start_link(__MODULE__, state, name: __MODULE__)
end
@impl true
def init(state) do
update_metrics()
# 5 minutes
Process.send_after(self(), :update, 5 * 60 * 1000)
{:ok, state}
end
@impl true
def handle_info(:update, state) do
update_metrics()
{:noreply, state}
end
def update_metrics do
pages = Repo.aggregate(Page, :count)
uploads = Repo.aggregate(Upload, :count)
links = Repo.aggregate(PageLink, :count)
:telemetry.execute([:wiki, :pages], %{count: pages})
:telemetry.execute([:wiki, :uploads], %{count: uploads})
:telemetry.execute([:wiki, :links], %{count: links})
end
end

View File

@ -0,0 +1,66 @@
defmodule Wiki.MetricStorage do
use GenServer
@history_buffer_size 50
def metrics_history(metric) do
GenServer.call(__MODULE__, {:data, metric})
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
@impl true
def init(metrics) do
Process.flag(:trap_exit, true)
metric_histories_map =
metrics
|> Enum.map(fn metric ->
attach_handler(metric)
{metric, CircularBuffer.new(@history_buffer_size)}
end)
|> Map.new()
{:ok, metric_histories_map}
end
@impl true
def terminate(_, metrics) do
for metric <- metrics do
:telemetry.detach({__MODULE__, metric, self()})
end
:ok
end
defp attach_handler(%{event_name: name_list} = metric) do
:telemetry.attach(
{__MODULE__, metric, self()},
name_list,
&__MODULE__.handle_event/4,
metric
)
end
def handle_event(_event_name, data, metadata, metric) do
if data = Phoenix.LiveDashboard.extract_datapoint_for_metric(metric, data, metadata) do
GenServer.cast(__MODULE__, {:telemetry_metric, data, metric})
end
end
@impl true
def handle_cast({:telemetry_metric, data, metric}, state) do
{:noreply, update_in(state[metric], &CircularBuffer.insert(&1, data))}
end
@impl true
def handle_call({:data, metric}, _from, state) do
if history = state[metric] do
{:reply, CircularBuffer.to_list(history), state}
else
{:reply, [], state}
end
end
end

View File

@ -143,6 +143,19 @@ defmodule WikiWeb.UserAuth do
end
end
def require_admin_user(conn, _opts) do
case conn.assigns[:current_user] do
%Accounts.User{is_admin: true} ->
conn
_ ->
conn
|> put_flash(:error, "You must be logged in as an admin to access this page.")
|> redirect(to: "/")
|> halt()
end
end
defp maybe_store_return_to(%{method: "GET", request_path: request_path} = conn) do
put_session(conn, :user_return_to, request_path)
end

View File

@ -2,6 +2,7 @@ defmodule WikiWeb.Router do
use WikiWeb, :router
import WikiWeb.UserAuth
import Phoenix.LiveDashboard.Router
pipeline :browser do
plug :accepts, ["html"]
@ -21,22 +22,6 @@ defmodule WikiWeb.Router do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: WikiWeb.Telemetry
end
end
## Authentication routes
scope "/", WikiWeb do
@ -68,6 +53,14 @@ defmodule WikiWeb.Router do
delete "/pages/:id/uploads/:upload_id", PageController, :delete_upload
end
scope "/", WikiWeb do
pipe_through [:browser, :require_authenticated_user, :require_admin_user]
live_dashboard "/dashboard",
metrics: WikiWeb.Telemetry,
metrics_history: {Wiki.MetricStorage, :metrics_history, []}
end
scope "/", WikiWeb do
pipe_through [:browser]

View File

@ -21,6 +21,11 @@ defmodule WikiWeb.Telemetry do
def metrics do
[
# Wiki Metrics
last_value("wiki.pages.count"),
last_value("wiki.uploads.count"),
last_value("wiki.links.count"),
# Phoenix Metrics
summary("phoenix.endpoint.stop.duration",
unit: {:native, :millisecond}

View File

@ -17,9 +17,9 @@
<%= if @current_user do %>
<li><%= link "New Page", to: Routes.page_path(@conn, :new) %></li>
<li><%= link "Random", to: Routes.page_path(@conn, :random) %></li>
<% end %>
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
<li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
<%= if @current_user.is_admin do %>
<li><%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %></li>
<% end %>
<% end %>
</ul>
<%= render "_user_menu.html", assigns %>

View File

@ -28,9 +28,10 @@
<% end %>
</table>
<%= form_tag Routes.page_path(@conn, :create_upload, @page.id), method: :post, multipart: true do %>
<%= form_tag Routes.page_path(@conn, :create_upload, @page.id), method: :post, multipart: true, class: "edit-upload-form" do %>
<input type="file" name="file">
<%= submit "Upload" %>
<progress value="0" max="1" style="visibility: hidden;"></progress>
<% end %>
<span><%= link "Back", to: Routes.page_path(@conn, :show, @page.id) %></span>

View File

@ -47,7 +47,8 @@ defmodule Wiki.MixProject do
{:jason, "~> 1.0"},
{:plug_cowboy, "~> 2.0"},
{:phx_gen_auth, "~> 0.4.0", only: :dev, runtime: false},
{:earmark, "~> 1.4.10"}
{:earmark, "~> 1.4.10"},
{:circular_buffer, "~> 0.3.0"}
]
end

View File

@ -1,6 +1,7 @@
%{
"argon2_elixir": {:hex, :argon2_elixir, "2.3.0", "e251bdafd69308e8c1263e111600e6d68bd44f23d2cccbe43fcb1a417a76bc8e", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "28ccb63bff213aecec1f7f3dde9648418b031f822499973281d8f494b9d5a3b3"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.2.0", "3df902b81ce7fa8867a2ae30d20a1da6877a2c056bfb116fd0bc8a5f0190cea4", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "762be3fcb779f08207531bc6612cca480a338e4b4357abb49f5ce00240a77d1e"},
"circular_buffer": {:hex, :circular_buffer, "0.3.0", "3eaa7e349ab07cff53dd1c25eb179ac14913fe1e407762260402a291dac4af96", [:mix], [], "hexpm", "f582b2ce394f8965cadf43c5b140f399b83dc8404aef701cb41c928c4bac1943"},
"comeonin": {:hex, :comeonin, "5.3.1", "7fe612b739c78c9c1a75186ef2d322ce4d25032d119823269d0aa1e2f1e20025", [:mix], [], "hexpm", "d6222483060c17f0977fad1b7401ef0c5863c985a64352755f366aee3799c245"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},

View File

@ -0,0 +1,9 @@
defmodule Wiki.Repo.Migrations.UsersAddIsAdmin do
use Ecto.Migration
def change do
alter table(:users) do
add :is_admin, :boolean, default: false
end
end
end