diff --git a/lib/philomena/bans.ex b/lib/philomena/bans.ex index 2f907a60..44d4251c 100644 --- a/lib/philomena/bans.ex +++ b/lib/philomena/bans.ex @@ -49,9 +49,9 @@ defmodule Philomena.Bans do {:error, %Ecto.Changeset{}} """ - def create_fingerprint(attrs \\ %{}) do - %Fingerprint{} - |> Fingerprint.changeset(attrs) + def create_fingerprint(creator, attrs \\ %{}) do + %Fingerprint{banning_user_id: creator.id} + |> Fingerprint.save_changeset(attrs) |> Repo.insert() end @@ -69,7 +69,7 @@ defmodule Philomena.Bans do """ def update_fingerprint(%Fingerprint{} = fingerprint, attrs) do fingerprint - |> Fingerprint.changeset(attrs) + |> Fingerprint.save_changeset(attrs) |> Repo.update() end diff --git a/lib/philomena/bans/fingerprint.ex b/lib/philomena/bans/fingerprint.ex index 1ede621a..ec9e5a44 100644 --- a/lib/philomena/bans/fingerprint.ex +++ b/lib/philomena/bans/fingerprint.ex @@ -3,6 +3,7 @@ defmodule Philomena.Bans.Fingerprint do import Ecto.Changeset alias Philomena.Users.User + alias RelativeDate.Parser schema "fingerprint_bans" do belongs_to :banning_user, User @@ -14,13 +15,47 @@ defmodule Philomena.Bans.Fingerprint do field :fingerprint, :string field :generated_ban_id, :string + field :until, :string, virtual: true + timestamps(inserted_at: :created_at) end @doc false - def changeset(fingerprint, attrs) do - fingerprint + def changeset(fingerprint_ban, attrs) do + fingerprint_ban |> cast(attrs, []) - |> validate_required([]) + |> populate_until() end + + def save_changeset(fingerprint_ban, attrs) do + fingerprint_ban + |> cast(attrs, [:reason, :note, :enabled, :fingerprint, :until]) + |> populate_valid_until() + |> put_ban_id() + |> validate_required([:reason, :enabled, :fingerprint, :valid_until]) + end + + defp populate_until(%{data: data} = changeset) do + put_change(changeset, :until, to_string(data.valid_until)) + end + + defp populate_valid_until(changeset) do + changeset + |> get_field(:until) + |> Parser.parse() + |> case do + {:ok, time} -> + change(changeset, valid_until: time) + + {:error, _err} -> + add_error(changeset, :until, "is not a valid absolute or relative date and time") + end + end + + defp put_ban_id(%{data: %{generated_ban_id: nil}} = changeset) do + ban_id = Base.encode16(:crypto.strong_rand_bytes(3)) + + put_change(changeset, :generated_ban_id, "F#{ban_id}") + end + defp put_ban_id(changeset), do: changeset end diff --git a/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex new file mode 100644 index 00000000..e7f14410 --- /dev/null +++ b/lib/philomena_web/controllers/admin/fingerprint_ban_controller.ex @@ -0,0 +1,96 @@ +defmodule PhilomenaWeb.Admin.FingerprintBanController do + use PhilomenaWeb, :controller + + alias Philomena.Bans.Fingerprint, as: FingerprintBan + alias Philomena.Bans + alias Philomena.Repo + import Ecto.Query + + plug :verify_authorized + plug :load_resource, model: FingerprintBan, only: [:edit, :update, :delete] + + def index(conn, %{"q" => q}) when is_binary(q) do + FingerprintBan + |> where([fb], + ilike(fb.fingerprint, ^"%#{q}%") + or fb.generated_ban_id == ^q + or fragment("to_tsvector(?) @@ plainto_tsquery(?)", fb.reason, ^q) + or fragment("to_tsvector(?) @@ plainto_tsquery(?)", fb.note, ^q) + ) + |> load_bans(conn) + end + + def index(conn, %{"fingerprint" => fingerprint}) when is_binary(fingerprint) do + FingerprintBan + |> where(fingerprint: ^fingerprint) + |> load_bans(conn) + end + + def index(conn, _params) do + load_bans(FingerprintBan, conn) + end + + def new(conn, %{"fingerprint" => fingerprint}) do + changeset = Bans.change_fingerprint(%FingerprintBan{fingerprint: fingerprint}) + render(conn, "new.html", changeset: changeset) + end + + def new(conn, _params) do + changeset = Bans.change_fingerprint(%FingerprintBan{}) + render(conn, "new.html", changeset: changeset) + end + + def create(conn, %{"fingerprint" => fingerprint_ban_params}) do + case Bans.create_fingerprint(conn.assigns.current_user, fingerprint_ban_params) do + {:ok, _fingerprint_ban} -> + conn + |> put_flash(:info, "Fingerprint was successfully banned.") + |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) + + {:error, changeset} -> + render(conn, "new.html", changeset: changeset) + end + end + + def edit(conn, _params) do + changeset = Bans.change_fingerprint(conn.assigns.fingerprint) + render(conn, "edit.html", changeset: changeset) + end + + def update(conn, %{"fingerprint" => fingerprint_ban_params}) do + case Bans.update_fingerprint(conn.assigns.fingerprint, fingerprint_ban_params) do + {:ok, _fingerprint_ban} -> + conn + |> put_flash(:info, "Fingerprint ban successfully updated.") + |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) + + {:error, changeset} -> + render(conn, "edit.html", changeset: changeset) + end + end + + def delete(conn, _params) do + {:ok, _fingerprint_ban} = Bans.delete_fingerprint(conn.assigns.fingerprint) + + conn + |> put_flash(:info, "Fingerprint ban successfully deleted.") + |> redirect(to: Routes.admin_fingerprint_ban_path(conn, :index)) + end + + defp load_bans(queryable, conn) do + fingerprint_bans = + queryable + |> order_by(desc: :created_at) + |> preload(:banning_user) + |> Repo.paginate(conn.assigns.scrivener) + + render(conn, "index.html", layout_class: "layout--wide", fingerprint_bans: fingerprint_bans) + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :index, FingerprintBan) do + true -> conn + false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end +end diff --git a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex index 96a819f1..38e8db24 100644 --- a/lib/philomena_web/controllers/admin/subnet_ban_controller.ex +++ b/lib/philomena_web/controllers/admin/subnet_ban_controller.ex @@ -46,7 +46,7 @@ defmodule PhilomenaWeb.Admin.SubnetBanController do case Bans.create_subnet(conn.assigns.current_user, subnet_ban_params) do {:ok, _subnet_ban} -> conn - |> put_flash(:info, "User was successfully banned.") + |> put_flash(:info, "Subnet was successfully banned.") |> redirect(to: Routes.admin_subnet_ban_path(conn, :index)) {:error, changeset} -> diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/_form.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/_form.html.slime new file mode 100644 index 00000000..30b4b38c --- /dev/null +++ b/lib/philomena_web/templates/admin/fingerprint_ban/_form.html.slime @@ -0,0 +1,22 @@ += form_for @changeset, @action, fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + .field + => label f, :fingerprint, "Fingerprint:" + = text_input f, :fingerprint, class: "input", placeholder: "Fingerprint", required: true + + .field + => label f, :reason, "Reason (shown to the banned user, and to staff on the user's profile page):" + = text_input f, :reason, class: "input input--wide", placeholder: "Reason", required: true + + .field + => label f, :note, "Admin-only note:" + = text_input f, :note, class: "input input--wide", placeholder: "Note" + + .field + => label f, :until, "End time relative to now, in simple English (e.g. \"1 week from now\"):" + = text_input f, :until, class: "input input--wide", placeholder: "Until", required: true + + = submit "Save Ban", class: "button" diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime new file mode 100644 index 00000000..2e78f09b --- /dev/null +++ b/lib/philomena_web/templates/admin/fingerprint_ban/edit.html.slime @@ -0,0 +1,6 @@ +h1 Editing ban + += render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: Routes.admin_fingerprint_ban_path(@conn, :update, @fingerprint), conn: @conn + +br += link "Back", to: Routes.admin_fingerprint_ban_path(@conn, :index) diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime new file mode 100644 index 00000000..ba74f8d8 --- /dev/null +++ b/lib/philomena_web/templates/admin/fingerprint_ban/index.html.slime @@ -0,0 +1,52 @@ +h1 Fingerprint Bans + +- route = fn p -> Routes.admin_fingerprint_ban_path(@conn, :index, p) end +- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @fingerprint_bans, route: route + +.block + .block__header + = pagination + + .block__content + table.table + thead + tr + th Fingerprint + th Created + th Expires + th Reason/Note + th Ban ID + th Options + + tbody + = for ban <- @fingerprint_bans do + tr + td + = link ban.fingerprint, to: Routes.fingerprint_profile_path(@conn, :show, ban.fingerprint) + + td + => pretty_time ban.created_at + = user_abbrv @conn, ban.banning_user + + td class=ban_row_class(ban) + = pretty_time ban.valid_until + + td + = ban.reason + + = if present?(ban.note) do + p.block.block--fixed + em + ' Note: + = ban.note + + td + = ban.generated_ban_id + + td + => link "Edit", to: Routes.admin_fingerprint_ban_path(@conn, :edit, ban) + ' • + => link "Destroy", to: Routes.admin_fingerprint_ban_path(@conn, :delete, ban), data: [confirm: "Are you really, really sure?", method: "delete"] + + .block__header.block__header--light + = pagination diff --git a/lib/philomena_web/templates/admin/fingerprint_ban/new.html.slime b/lib/philomena_web/templates/admin/fingerprint_ban/new.html.slime new file mode 100644 index 00000000..75db4f62 --- /dev/null +++ b/lib/philomena_web/templates/admin/fingerprint_ban/new.html.slime @@ -0,0 +1,5 @@ +h1 New Fingerprint Ban += render PhilomenaWeb.Admin.FingerprintBanView, "_form.html", changeset: @changeset, action: Routes.admin_fingerprint_ban_path(@conn, :create), conn: @conn + +br += link "Back", to: Routes.admin_fingerprint_ban_path(@conn, :index) diff --git a/lib/philomena_web/views/admin/fingerprint_ban_view.ex b/lib/philomena_web/views/admin/fingerprint_ban_view.ex new file mode 100644 index 00000000..530e031e --- /dev/null +++ b/lib/philomena_web/views/admin/fingerprint_ban_view.ex @@ -0,0 +1,14 @@ +defmodule PhilomenaWeb.Admin.FingerprintBanView do + use PhilomenaWeb, :view + + import PhilomenaWeb.ProfileView, only: [user_abbrv: 2] + + defp ban_row_class(%{valid_until: until, enabled: enabled}) do + now = DateTime.utc_now() + + case enabled and DateTime.diff(until, now) > 0 do + true -> "success" + _false -> "danger" + end + end +end