mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +01:00
user bans + ban messages for users
This commit is contained in:
parent
1da0087896
commit
4063f84b6b
19 changed files with 329 additions and 38 deletions
|
@ -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)
|
||||
]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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...
|
||||
#
|
||||
|
|
94
lib/philomena_web/controllers/admin/user_ban_controller.ex
Normal file
94
lib/philomena_web/controllers/admin/user_ban_controller.ex
Normal 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
|
|
@ -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
|
||||
|
|
26
lib/philomena_web/templates/admin/user_ban/_form.html.slime
Normal file
26
lib/philomena_web/templates/admin/user_ban/_form.html.slime
Normal 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"
|
|
@ -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)
|
58
lib/philomena_web/templates/admin/user_ban/index.html.slime
Normal file
58
lib/philomena_web/templates/admin/user_ban/index.html.slime
Normal 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)
|
||||
' •
|
||||
=> 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
|
|
@ -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)
|
16
lib/philomena_web/templates/ban/_ban_reason.html.slime
Normal file
16
lib/philomena_web/templates/ban/_ban_reason.html.slime
Normal 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
|
||||
' ).
|
|
@ -27,9 +27,14 @@ h1 = @conversation.title
|
|||
.block__header.block__header--light
|
||||
= pagination
|
||||
|
||||
= if @messages.total_entries < 1_000 do
|
||||
= 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
|
||||
- else
|
||||
|
||||
- true ->
|
||||
div
|
||||
h2 Okay, we're impressed
|
||||
p You've managed to send over 1,000 messages in this conversation!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
14
lib/philomena_web/views/admin/user_ban_view.ex
Normal file
14
lib/philomena_web/views/admin/user_ban_view.ex
Normal 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
|
Loading…
Reference in a new issue