user bans + ban messages for users

This commit is contained in:
byte[] 2019-12-13 12:14:34 -05:00
parent 1da0087896
commit 4063f84b6b
19 changed files with 329 additions and 38 deletions

View file

@ -241,9 +241,9 @@ defmodule Philomena.Bans do
{:error, %Ecto.Changeset{}}
"""
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
def create_user(creator, attrs \\ %{}) do
%User{banning_user_id: creator.id}
|> User.save_changeset(attrs)
|> Repo.insert()
end
@ -261,7 +261,7 @@ defmodule Philomena.Bans do
"""
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> User.save_changeset(attrs)
|> Repo.update()
end
@ -321,7 +321,7 @@ defmodule Philomena.Bans do
defp fingerprint_query(fingerprint, now) do
[
Fingerprint
|> select([f], %{reason: f.reason, valid_until: f.valid_until, generated_ban_id: f.generated_ban_id, type: "FingerprintBan"})
|> select([f], %{reason: f.reason, valid_until: f.valid_until, generated_ban_id: f.generated_ban_id, type: ^"FingerprintBan"})
|> where([f], f.enabled and f.valid_until > ^now)
|> where([f], f.fingerprint == ^fingerprint)
]
@ -333,7 +333,7 @@ defmodule Philomena.Bans do
[
Subnet
|> select([s], %{reason: s.reason, valid_until: s.valid_until, generated_ban_id: s.generated_ban_id, type: "SubnetBan"})
|> select([s], %{reason: s.reason, valid_until: s.valid_until, generated_ban_id: s.generated_ban_id, type: ^"SubnetBan"})
|> where([s], s.enabled and s.valid_until > ^now)
|> where(fragment("specification >>= ?", ^inet))
]
@ -343,7 +343,7 @@ defmodule Philomena.Bans do
defp user_query(user, now) do
[
User
|> select([u], %{reason: u.reason, valid_until: u.valid_until, generated_ban_id: u.generated_ban_id, type: "UserBan"})
|> select([u], %{reason: u.reason, valid_until: u.valid_until, generated_ban_id: u.generated_ban_id, type: ^"UserBan"})
|> where([u], u.enabled and u.valid_until > ^now)
|> where([u], u.user_id == ^user.id)
]

View file

@ -10,7 +10,7 @@ defmodule Philomena.Bans.Fingerprint do
field :reason, :string
field :note, :string
field :enabled, :boolean, default: true
field :valid_until, :naive_datetime
field :valid_until, :utc_datetime
field :fingerprint, :string
field :generated_ban_id, :string

View file

@ -10,7 +10,7 @@ defmodule Philomena.Bans.Subnet do
field :reason, :string
field :note, :string
field :enabled, :boolean, default: true
field :valid_until, :naive_datetime
field :valid_until, :utc_datetime
field :specification, EctoNetwork.INET
field :generated_ban_id, :string

View file

@ -3,6 +3,8 @@ defmodule Philomena.Bans.User do
import Ecto.Changeset
alias Philomena.Users.User
alias Philomena.Repo
alias RelativeDate.Parser
schema "user_bans" do
belongs_to :user, User
@ -11,17 +13,71 @@ defmodule Philomena.Bans.User do
field :reason, :string
field :note, :string
field :enabled, :boolean, default: true
field :valid_until, :naive_datetime
field :valid_until, :utc_datetime
field :generated_ban_id, :string
field :override_ip_ban, :boolean, default: false
field :username, :string, virtual: true
field :until, :string, virtual: true
timestamps(inserted_at: :created_at)
end
@doc false
def changeset(user, attrs) do
user
def changeset(user_ban, attrs) do
user_ban
|> cast(attrs, [])
|> validate_required([])
|> populate_until()
|> populate_username()
end
def save_changeset(user_ban, attrs) do
user_ban
|> cast(attrs, [:reason, :note, :enabled, :override_ip_ban, :username, :until])
|> populate_valid_until()
|> populate_user_id()
|> put_ban_id()
|> validate_required([:reason, :enabled, :user_id, :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 populate_username(changeset) do
case maybe_get_by(:id, get_field(changeset, :user_id)) do
nil -> changeset
user -> put_change(changeset, :username, user.name)
end
end
defp populate_user_id(changeset) do
case maybe_get_by(:name, get_field(changeset, :username)) do
nil -> changeset
%{id: id} -> put_change(changeset, :user_id, id)
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, "U#{ban_id}")
end
defp put_ban_id(changeset), do: changeset
defp maybe_get_by(_field, nil), do: nil
defp maybe_get_by(field, value), do: Repo.get_by(User, [{field, value}])
end

View file

@ -16,6 +16,10 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
alias Philomena.Tags.Tag
alias Philomena.Reports.Report
alias Philomena.Bans.User, as: UserBan
alias Philomena.Bans.Subnet, as: SubnetBan
alias Philomena.Bans.Fingerprint, as: FingerprintBan
# Admins can do anything
def can?(%User{role: "admin"}, _action, _model), do: true
@ -68,6 +72,11 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{role: "moderator"}, :show_reason, %DnpEntry{}), do: true
def can?(%User{role: "moderator"}, :show_feedback, %DnpEntry{}), do: true
# Create bans
def can?(%User{role: "moderator"}, _action, UserBan), do: true
def can?(%User{role: "moderator"}, _action, SubnetBan), do: true
def can?(%User{role: "moderator"}, _action, FingerprintBan), do: true
#
# Assistants can...
#

View file

@ -0,0 +1,94 @@
defmodule PhilomenaWeb.Admin.UserBanController do
use PhilomenaWeb, :controller
alias Philomena.Bans.User, as: UserBan
alias Philomena.Bans
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized
plug :load_resource, model: UserBan, only: [:edit, :update, :delete]
def index(conn, %{"q" => q}) when is_binary(q) do
like_q = "%#{q}%"
UserBan
|> join(:inner, [ub], _ in assoc(ub, :user))
|> where([ub, u],
ilike(u.name, ^like_q)
or ub.generated_ban_id == ^q
or fragment("to_tsvector(?) @@ plainto_tsquery(?)", ub.reason, ^q)
or fragment("to_tsvector(?) @@ plainto_tsquery(?)", ub.note, ^q)
)
|> load_bans(conn)
end
def index(conn, %{"user_id" => user_id}) when is_binary(user_id) do
UserBan
|> where(user_id: ^user_id)
|> load_bans(conn)
end
def index(conn, _params) do
load_bans(UserBan, conn)
end
def new(conn, _params) do
changeset = Bans.change_user(%UserBan{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user" => user_ban_params}) do
case Bans.create_user(conn.assigns.current_user, user_ban_params) do
{:ok, _user_ban} ->
conn
|> put_flash(:info, "User was successfully banned.")
|> redirect(to: Routes.admin_user_ban_path(conn, :index))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def edit(conn, _params) do
changeset = Bans.change_user(conn.assigns.user)
render(conn, "edit.html", changeset: changeset)
end
def update(conn, %{"user" => user_ban_params}) do
case Bans.update_user(conn.assigns.user, user_ban_params) do
{:ok, _user_ban} ->
conn
|> put_flash(:info, "User ban successfully updated.")
|> redirect(to: Routes.admin_user_ban_path(conn, :index))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
end
end
def delete(conn, _params) do
{:ok, _user_ban} = Bans.delete_user(conn.assigns.user)
conn
|> put_flash(:info, "User ban successfully deleted.")
|> redirect(to: Routes.admin_user_ban_path(conn, :index))
end
defp load_bans(queryable, conn) do
user_bans =
queryable
|> order_by(desc: :created_at)
|> preload([:user, :banning_user])
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", layout_class: "layout--wide", user_bans: user_bans)
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, UserBan) do
true -> conn
false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -190,6 +190,10 @@ defmodule PhilomenaWeb.Router do
resources "/dnp_entries", DnpEntryController, only: [:index] do
resources "/transition", DnpEntry.TransitionController, only: [:create], singleton: true
end
resources "/user_bans", UserBanController, only: [:index, :new, :create, :edit, :update, :delete]
resources "/subnet_bans", SubnetBanController, only: [:index, :new, :create, :edit, :update, :delete]
resources "/fingerprint_bans", FingerprintBanController, only: [:index, :new, :create, :edit, :update, :delete]
end
resources "/duplicate_reports", DuplicateReportController, only: [] do

View file

@ -0,0 +1,26 @@
= 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, :username, "Username:"
= text_input f, :username, class: "input", placeholder: "Username", required: true
.field
=> checkbox f, :override_ip_ban, class: "checkbox"
= label f, :override_ip_ban, "Override IP ban?"
.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"

View file

@ -0,0 +1,6 @@
h1 Editing ban
= render PhilomenaWeb.Admin.UserBanView, "_form.html", changeset: @changeset, action: Routes.admin_user_ban_path(@conn, :update, @user), conn: @conn
br
= link "Back", to: Routes.admin_user_ban_path(@conn, :index)

View file

@ -0,0 +1,58 @@
h1 User Bans
- route = fn p -> Routes.admin_user_ban_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @user_bans, route: route, params: @conn.query_params
.block
.block__header
= pagination
.block__content
table.table
thead
tr
th User
th Created
th Expires
th Reason/Note
th Ban ID
th Auto IP Ban
th Options
tbody
= for ban <- @user_bans do
tr
td
= link ban.user.name, to: Routes.profile_path(@conn, :show, ban.user)
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
= if ban.override_ip_ban do
td.danger Disabled
- else
td.success Enabled
td
=> link "Edit", to: Routes.admin_user_ban_path(@conn, :edit, ban)
' &bull;
=> link "Destroy", to: Routes.admin_user_ban_path(@conn, :delete, ban), data: [confirm: "Are you really, really sure?", method: "delete"]
.block__header.block__header--light
= pagination

View file

@ -0,0 +1,5 @@
h1 New User Ban
= render PhilomenaWeb.Admin.UserBanView, "_form.html", changeset: @changeset, action: Routes.admin_user_ban_path(@conn, :create), conn: @conn
br
= link "Back", to: Routes.admin_user_ban_path(@conn, :index)

View file

@ -0,0 +1,16 @@
.block.block--fixed.block--warning
h4
' You've been banned!
p
' You cannot create comments or posts or update metadata (or do anything but read, really) until
= pretty_time @conn.assigns.current_ban.valid_until
' .
p
' The reason given by the administrator who banned you is:
br
strong>
= @conn.assigns.current_ban.reason
' (Ban ID:
= @conn.assigns.current_ban.generated_ban_id
' ).

View file

@ -111,4 +111,4 @@ table.table
td Literal
td Matches comments with the specified user_id. Anonymous users will never match this term.
td
code = link "user_id:211190", to: Routes.comment_path(@conn, :index, cq: "user_id:211190")
code = link "user_id:211190", to: Routes.comment_path(@conn, :index, cq: "user_id:211190")

View file

@ -27,13 +27,18 @@ h1 = @conversation.title
.block__header.block__header--light
= pagination
= if @messages.total_entries < 1_000 do
= render PhilomenaWeb.Conversation.MessageView, "_form.html", conversation: @conversation, changeset: @changeset, conn: @conn
- else
div
h2 Okay, we're impressed
p You've managed to send over 1,000 messages in this conversation!
p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it.
p
=> link "Click here", to: Routes.conversation_path(@conn, :new, receipient: other.name)
' to make a new conversation with this user.
= cond do
- @conn.assigns.current_ban ->
= render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn
- @messages.total_entries < 1_000 ->
= render PhilomenaWeb.Conversation.MessageView, "_form.html", conversation: @conversation, changeset: @changeset, conn: @conn
- true ->
div
h2 Okay, we're impressed
p You've managed to send over 1,000 messages in this conversation!
p We'd like to ask you to make a new conversation. Don't worry, this one won't go anywhere if you need to refer back to it.
p
=> link "Click here", to: Routes.conversation_path(@conn, :new, receipient: other.name)
' to make a new conversation with this user.

View file

@ -13,7 +13,7 @@ elixir:
i.fa.fa-sync
span.hide-mobile<> Refresh
= for {comment, body} <- @comments do
= for {comment, body} <- @comments, not comment.destroyed_content or can?(@conn, :show, comment) do
= render PhilomenaWeb.CommentView, "_comment.html", comment: comment, body: body, conn: @conn
.block

View file

@ -16,12 +16,7 @@
h4 Comments
= cond do
- @conn.assigns.current_ban ->
.block.block--fixed.block--warning
h4 You've been banned!
p
' You cannot post comments or update metadata (or do anything but
' read, really) until
= pretty_time(@conn.assigns.current_ban.valid_until)
= render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn
- @image.commenting_allowed ->
= render PhilomenaWeb.Image.CommentView, "_form.html", image: @image, changeset: @comment_changeset, conn: @conn
@ -29,4 +24,4 @@
- true ->
#comments data-current-url=Routes.image_comment_path(@conn, :index, @image, page: 1) data-loaded="true"
= render PhilomenaWeb.Image.CommentView, "index.html", image: @image, comments: @comments, conn: @conn
= render PhilomenaWeb.Image.CommentView, "index.html", image: @image, comments: @comments, conn: @conn

View file

@ -71,15 +71,15 @@
' B
i.fa.fa-caret-down
.dropdown__content.dropdown__content-right.js-burger-links
= if can?(@conn, :mod_read, UserBan) do
= link to: "#", class: "header__link" do
= if can?(@conn, :index, UserBan) do
= link to: Routes.admin_user_ban_path(@conn, :index), class: "header__link" do
i.fa.fa-fw.fa-user>
' User Bans
= if can?(@conn, :mod_read, SubnetBan) do
= link to: "#", class: "header__link" do
= if can?(@conn, :index, SubnetBan) do
= link to: Routes.admin_subnet_ban_path(@conn, :index), class: "header__link" do
i.fab.fa-fw.fa-internet-explorer>
' IP Bans
= if can?(@conn, :mod_read, FingerprintBan) do
= link to: "#", class: "header__link" do
= if can?(@conn, :index, FingerprintBan) do
= link to: Routes.admin_fingerprint_ban_path(@conn, :index), class: "header__link" do
i.fa.fa-fw.fa-desktop>
' FP Bans

View file

@ -62,6 +62,9 @@ h1 = @topic.title
/ Post form
= cond do
- @conn.assigns.current_ban ->
= render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn
- @topic.post_count < 200_000 and can?(@conn, :create_post, @topic) ->
= render PhilomenaWeb.Topic.PostView, "_form.html", conn: @conn, forum: @forum, topic: @topic, changeset: @changeset

View file

@ -0,0 +1,14 @@
defmodule PhilomenaWeb.Admin.UserBanView 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