filter editor

This commit is contained in:
byte[] 2019-11-02 09:14:03 -04:00
parent 4d7ef4a18d
commit 2c04e1cf3d
14 changed files with 239 additions and 39 deletions

View file

@ -18,7 +18,7 @@ config :philomena, :pow,
user: Philomena.Users.User, user: Philomena.Users.User,
repo: Philomena.Repo, repo: Philomena.Repo,
web_module: PhilomenaWeb, web_module: PhilomenaWeb,
extensions: [PowResetPassword, PowPersistentSession, PowMultiFactor], extensions: [PowResetPassword, PowPersistentSession],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
config :bcrypt_elixir, config :bcrypt_elixir,

View file

@ -64,8 +64,8 @@ defmodule Philomena.Filters do
{:error, %Ecto.Changeset{}} {:error, %Ecto.Changeset{}}
""" """
def create_filter(attrs \\ %{}) do def create_filter(user, attrs \\ %{}) do
%Filter{} %Filter{user_id: user.id}
|> Filter.changeset(attrs) |> Filter.changeset(attrs)
|> Repo.insert() |> Repo.insert()
end end

View file

@ -1,9 +1,15 @@
defmodule Philomena.Filters.Filter do defmodule Philomena.Filters.Filter do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query
alias Philomena.Tags.Tag
alias Philomena.Images.Query
alias Philomena.Users.User
alias Philomena.Repo
schema "filters" do schema "filters" do
belongs_to :user, Philomena.Users.User belongs_to :user, User
field :name, :string field :name, :string
field :description, :string field :description, :string
@ -15,13 +21,110 @@ defmodule Philomena.Filters.Filter do
field :spoilered_tag_ids, {:array, :integer}, default: [] field :spoilered_tag_ids, {:array, :integer}, default: []
field :user_count, :integer, default: 0 field :user_count, :integer, default: 0
field :spoilered_tag_list, :string, virtual: true
field :hidden_tag_list, :string, virtual: true
timestamps(inserted_at: :created_at) timestamps(inserted_at: :created_at)
end end
@doc false @doc false
def changeset(filter, attrs) do def changeset(filter, attrs) do
filter filter
|> cast(attrs, []) |> cast(attrs, [:spoilered_tag_list, :hidden_tag_list, :description, :name, :spoilered_complex_str, :hidden_complex_str])
|> validate_required([]) |> propagate_tag_lists()
|> validate_required([:name])
|> unsafe_validate_unique([:user_id, :name], Repo)
|> validate_my_downvotes(:spoilered_complex_str)
|> validate_my_downvotes(:hidden_complex_str)
|> validate_search(:spoilered_complex_str)
|> validate_search(:hidden_complex_str)
end
def assign_tag_lists(filter) do
tags = Enum.uniq(filter.spoilered_tag_ids ++ filter.hidden_tag_ids)
lookup =
Tag
|> where([t], t.id in ^tags)
|> Repo.all()
|> Map.new(fn t -> {t.id, t.name} end)
spoilered_tag_list =
filter.spoilered_tag_ids
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
|> Enum.sort()
|> Enum.join(", ")
hidden_tag_list =
filter.hidden_tag_ids
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
|> Enum.sort()
|> Enum.join(", ")
%{filter | hidden_tag_list: hidden_tag_list, spoilered_tag_list: spoilered_tag_list}
end
defp propagate_tag_lists(changeset) do
spoilers = get_field(changeset, :spoilered_tag_list) |> parse_tag_list
filters = get_field(changeset, :hidden_tag_list) |> parse_tag_list
tags = Enum.uniq(spoilers ++ filters)
lookup =
Tag
|> where([t], t.name in ^tags)
|> Repo.all()
|> Map.new(fn t -> {t.name, t.id} end)
spoilered_tag_ids =
spoilers
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
hidden_tag_ids =
filters
|> Enum.map(&lookup[&1])
|> Enum.filter(& &1 != nil)
changeset
|> put_change(:spoilered_tag_ids, spoilered_tag_ids)
|> put_change(:hidden_tag_ids, hidden_tag_ids)
end
defp validate_my_downvotes(changeset, field) do
value = get_field(changeset, field) || ""
if String.match?(value, ~r/my:downvotes/i) do
changeset
|> add_error(field, "cannot contain my:downvotes")
else
changeset
end
end
defp validate_search(changeset, field) do
user_id = get_field(changeset, :user_id)
if user_id do
user = User |> Repo.get!(user_id)
output = Query.user_parser(user, get_field(changeset, field))
case output do
{:ok, _} -> changeset
_ ->
changeset
|> add_error(field, "is invalid")
end
else
changeset
end
end
defp parse_tag_list(list) do
(list || "")
|> String.split(",")
|> Enum.map(&String.trim(&1))
|> Enum.filter(& &1 != "")
end end
end end

View file

@ -13,6 +13,21 @@ defmodule Philomena.Users.Encryptor do
:crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, msg, "", auth_tag, false) :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, msg, "", auth_tag, false)
end end
def encrypt_model(secret) do
salt = :crypto.strong_rand_bytes(16)
iv = :crypto.strong_rand_bytes(12)
{:ok, key} = :pbkdf2.pbkdf2(:sha, otp_secret(), salt, 2000, 32)
{msg, auth_tag} = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, secret, "", true)
# attr_encrypted encoding scheme
%{
secret: Base.encode64(<<msg::binary, auth_tag::binary>>) <> "\n",
salt: "_" <> Base.encode64(salt) <> "\n",
iv: Base.encode64(iv) <> "\n"
}
end
defp otp_secret do defp otp_secret do
Application.get_env(:philomena, :otp_secret_key) Application.get_env(:philomena, :otp_secret_key)
end end

View file

@ -106,7 +106,6 @@ defmodule Philomena.Users.User do
|> pow_extension_changeset(attrs) |> pow_extension_changeset(attrs)
|> cast(attrs, []) |> cast(attrs, [])
|> validate_required([]) |> validate_required([])
|> put_otp_secret()
end end
def otp_secret(%{encrypted_otp_secret: x} = user) when x not in [nil, ""] do def otp_secret(%{encrypted_otp_secret: x} = user) when x not in [nil, ""] do
@ -119,9 +118,14 @@ defmodule Philomena.Users.User do
def otp_secret(_user), do: nil def otp_secret(_user), do: nil
defp put_otp_secret(%{valid?: true} = changeset) do def put_otp_secret(user_or_changeset, secret) do
data = Philomena.Users.Encryptor.encrypt_model(secret)
user_or_changeset
|> change(%{
encrypted_otp_secret: data.secret,
encrypted_otp_secret_iv: data.iv,
encrypted_otp_secret_salt: data.salt
})
end end
defp put_otp_secret(changeset), do: changeset
end end

View file

@ -67,7 +67,7 @@ defmodule PhilomenaWeb.FilterController do
end end
def edit(conn, _params) do def edit(conn, _params) do
filter = conn.assigns.filter filter = conn.assigns.filter |> Filter.assign_tag_lists()
changeset = Filters.change_filter(filter) changeset = Filters.change_filter(filter)
render(conn, "edit.html", filter: filter, changeset: changeset) render(conn, "edit.html", filter: filter, changeset: changeset)

View file

@ -1,6 +1,5 @@
defmodule PhilomenaWeb.Plugs.CurrentFilter do defmodule PhilomenaWeb.Plugs.CurrentFilter do
import Plug.Conn import Plug.Conn
import Ecto.Query
alias Philomena.{Filters, Filters.Filter} alias Philomena.{Filters, Filters.Filter}
alias Philomena.Repo alias Philomena.Repo
@ -22,7 +21,7 @@ defmodule PhilomenaWeb.Plugs.CurrentFilter do
filter = if filter_id, do: Repo.get(Filter, filter_id) filter = if filter_id, do: Repo.get(Filter, filter_id)
filter = filter || Filters.default_filter() filter || Filters.default_filter()
end end
conn conn

View file

@ -0,0 +1,56 @@
.form
= form_for @filter, @route, fn f ->
.field
= text_input f, :name, class: "input input--wide", placeholder: "Name"
.fieldlabel
' This is a friendly name for this filter - it should be short and descriptive.
.field
= textarea f, :description, class: "input input--wide", placeholder: "Description"
.fieldlabel
' Here you can describe your filter in a bit more detail.
.field
= label f, :spoilered_tag_list, "Spoilered Tags"
br
= render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :spoilered_tag_list, type: "edit"
.fieldlabel
' These tags will spoiler the images they're associated with.
.field
= label f, :spoilered_complex_str, "Complex Spoiler Filter"
br
= textarea f, :spoilered_complex_str, class: "input input--wide", autocapitalize: "none"
.fieldlabel
p
' Use the search syntax here to specify an additional filter.
' For multiple filters, separate with a newline (or use the OR operator). Search fields excepting
code<> faved_by
' are supported. See the search syntax guide
' for more information.
p
strong> WARNING:
' This filter is applied along with your tag filters. Tag filters may spoiler images that you mean to filter more precisely here. Double-check to make sure they don't interfere.
.field
= label f, :hidden_tag_list, "Hidden Tags"
br
= render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :hidden_tag_list, type: "edit"
.fieldlabel
' These tags will hide images entirely from the site; if you go directly to an image, it will spoiler it.
.field
= label f, :hidden_complex_str, "Complex Hide Filter"
br
= textarea f, :hidden_complex_str, class: "input input--wide", autocapitalize: "none"
.fieldlabel
p
' Use the search syntax here to specify an additional filter.
' For multiple filters, separate with a newline (or use the OR operator). Search fields excepting
code<> faved_by
' are supported. See the search syntax guide
' for more information.
p
strong> WARNING:
' This filter is applied along with your tag filters. Tag filters may hide images that you mean to filter more precisely here. Double-check to make sure they don't interfere.
= submit "Save Filter", class: "button"

View file

@ -0,0 +1,2 @@
h2 Editing Filter
= render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :update, @filter)

View file

@ -26,11 +26,10 @@
h2 My Filters h2 My Filters
= if @current_user do = if @current_user do
/- if can? :create, Filter p
/ p = link("Click here to make a new filter from scratch", to: Routes.filter_path(@conn, :new))
/ => link_to 'Click here to make a new filter from scratch', new_filter_path = for filter <- @my_filters do
/ | or click "Copy and Customize" on a global or shared filter to use as a starting point. = render PhilomenaWeb.FilterView, "_filter.html", conn: @conn, filter: filter
/= render partial: 'filter_list', locals: { filters: @user_filters }
- else - else
p p
' If you're logged in, you can create and maintain custom filters here. ' If you're logged in, you can create and maintain custom filters here.

View file

@ -0,0 +1,2 @@
h2 Creating New Filter
= render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :create)

View file

@ -0,0 +1,9 @@
.js-tag-block class="fancy-tag-#{@type}"
= textarea @f, @name, class: "input input--wide tagsinput js-image-input js-taginput js-taginput-plain js-taginput-#{@name}", placeholder: "Add tags separated with commas"
.js-taginput.input.input--wide.tagsinput.hidden class="js-taginput-fancy" data-click-focus=".js-taginput-input.js-taginput-#{@name}"
input.input class="js-taginput-input js-taginput-#{@name}" id="taginput-fancy-#{@name}" type="text" placeholder="add a tag" autocapitalize="none" data-ac="true" data-ac-min-length="3" data-ac-source='/tags/autocomplete.json?term='
button.button.button--state-primary.button--bold class="js-taginput-show" data-click-show=".js-taginput-fancy,.js-taginput-hide" data-click-hide=".js-taginput-plain,.js-taginput-show" data-click-focus=".js-taginput-input.js-taginput-#{@name}"
= hidden_input :fuck_ie, :fuck_ie, value: "fuck_ie"
' Fancy Editor
button.hidden.button.button--state-primary.button--bold class="js-taginput-hide" data-click-show=".js-taginput-plain,.js-taginput-show" data-click-hide=".js-taginput-fancy,.js-taginput-hide" data-click-focus=".js-taginput-plain.js-taginput-#{@name}"
' Plain Editor

View file

@ -25,29 +25,29 @@ defmodule PowMultiFactor.Phoenix.ControllerCallbacks do
alias Pow.Plug alias Pow.Plug
alias PowMultiFactor.Plug, as: PowMultiFactorPlug alias PowMultiFactor.Plug, as: PowMultiFactorPlug
def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, _config) do def before_respond(Pow.Phoenix.SessionController, :create, {:ok, conn}, config) do
return_path = routes(conn).session_path(conn, :new) return_path = routes(conn).session_path(conn, :new)
clear_unauthorized(conn, {:ok, conn}, return_path) clear_unauthorized(conn, config, {:ok, conn}, return_path)
end end
def before_respond(Pow.Phoenix.RegistrationController, :update, {:ok, user, conn}, _config) do def before_respond(Pow.Phoenix.RegistrationController, :update, {:ok, user, conn}, config) do
return_path = routes(conn).registration_path(conn, :edit) return_path = routes(conn).registration_path(conn, :edit)
halt_unauthorized(conn, {:ok, user, conn}, return_path) halt_unauthorized(conn, config, {:ok, user, conn}, return_path)
end end
defp clear_unauthorized(conn, success_response, return_path) do defp clear_unauthorized(conn, config, success_response, return_path) do
case PowMultiFactorPlug.mfa_unauthorized?(conn) do case PowMultiFactorPlug.mfa_authorized?(conn, config) do
true -> clear_auth(conn) |> go_back(return_path) false -> clear_auth(conn) |> go_back(return_path)
false -> success_response true -> success_response
end end
end end
defp halt_unauthorized(conn, success_response, return_path) do defp halt_unauthorized(conn, config, success_response, return_path) do
case PowMultiFactorPlug.mfa_unauthorized?(conn) do case PowMultiFactorPlug.mfa_authorized?(conn, config) do
true -> go_back(conn, return_path) false -> go_back(conn, return_path)
false -> success_response true -> success_response
end end
end end

View file

@ -3,24 +3,35 @@ defmodule PowMultiFactor.Plug do
Plug helper methods. Plug helper methods.
""" """
alias Plug.Crypto
alias Pow.Plug alias Pow.Plug
#alias PowMultiFactor.Ecto.Context alias Pow.Config
def mfa_unauthorized?(conn) do def mfa_authorized?(conn, config) do
user = Plug.current_user(conn) user = Plug.current_user(conn)
if user.otp_required_for_login do if user.otp_required_for_login do
true secret = user.__struct__.otp_secret(user)
totp = Elixir2fa.generate_totp(secret)
Crypto.secure_compare(totp, conn.params)
else else
false true
end end
end end
#defp otp_secret(user) do def assign_mfa(conn, config) do
user = Plug.current_user(conn)
repo = Config.repo!(config)
#end if user.encrypted_otp_secret in [nil, ""] do
{:ok, user} =
user.__struct__.put_otp_secret(Elixir2fa.random_secret())
|> repo.update()
#defp otp_shared_key do user
# Application.get_env else
#end user
end
end
end end