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,
repo: Philomena.Repo,
web_module: PhilomenaWeb,
extensions: [PowResetPassword, PowPersistentSession, PowMultiFactor],
extensions: [PowResetPassword, PowPersistentSession],
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
config :bcrypt_elixir,

View file

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

View file

@ -1,9 +1,15 @@
defmodule Philomena.Filters.Filter do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
alias Philomena.Tags.Tag
alias Philomena.Images.Query
alias Philomena.Users.User
alias Philomena.Repo
schema "filters" do
belongs_to :user, Philomena.Users.User
belongs_to :user, User
field :name, :string
field :description, :string
@ -15,13 +21,110 @@ defmodule Philomena.Filters.Filter do
field :spoilered_tag_ids, {:array, :integer}, default: []
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)
end
@doc false
def changeset(filter, attrs) do
filter
|> cast(attrs, [])
|> validate_required([])
|> cast(attrs, [:spoilered_tag_list, :hidden_tag_list, :description, :name, :spoilered_complex_str, :hidden_complex_str])
|> 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

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)
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
Application.get_env(:philomena, :otp_secret_key)
end

View file

@ -106,7 +106,6 @@ defmodule Philomena.Users.User do
|> pow_extension_changeset(attrs)
|> cast(attrs, [])
|> validate_required([])
|> put_otp_secret()
end
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
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
defp put_otp_secret(changeset), do: changeset
end

View file

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

View file

@ -1,6 +1,5 @@
defmodule PhilomenaWeb.Plugs.CurrentFilter do
import Plug.Conn
import Ecto.Query
alias Philomena.{Filters, Filters.Filter}
alias Philomena.Repo
@ -22,7 +21,7 @@ defmodule PhilomenaWeb.Plugs.CurrentFilter do
filter = if filter_id, do: Repo.get(Filter, filter_id)
filter = filter || Filters.default_filter()
filter || Filters.default_filter()
end
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
= if @current_user do
/- if can? :create, Filter
/ p
/ => link_to 'Click here to make a new filter from scratch', new_filter_path
/ | or click "Copy and Customize" on a global or shared filter to use as a starting point.
/= render partial: 'filter_list', locals: { filters: @user_filters }
p
= link("Click here to make a new filter from scratch", to: Routes.filter_path(@conn, :new))
= for filter <- @my_filters do
= render PhilomenaWeb.FilterView, "_filter.html", conn: @conn, filter: filter
- else
p
' 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 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)
clear_unauthorized(conn, {:ok, conn}, return_path)
clear_unauthorized(conn, config, {:ok, conn}, return_path)
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)
halt_unauthorized(conn, {:ok, user, conn}, return_path)
halt_unauthorized(conn, config, {:ok, user, conn}, return_path)
end
defp clear_unauthorized(conn, success_response, return_path) do
case PowMultiFactorPlug.mfa_unauthorized?(conn) do
true -> clear_auth(conn) |> go_back(return_path)
false -> success_response
defp clear_unauthorized(conn, config, success_response, return_path) do
case PowMultiFactorPlug.mfa_authorized?(conn, config) do
false -> clear_auth(conn) |> go_back(return_path)
true -> success_response
end
end
defp halt_unauthorized(conn, success_response, return_path) do
case PowMultiFactorPlug.mfa_unauthorized?(conn) do
true -> go_back(conn, return_path)
false -> success_response
defp halt_unauthorized(conn, config, success_response, return_path) do
case PowMultiFactorPlug.mfa_authorized?(conn, config) do
false -> go_back(conn, return_path)
true -> success_response
end
end

View file

@ -3,24 +3,35 @@ defmodule PowMultiFactor.Plug do
Plug helper methods.
"""
alias Plug.Crypto
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)
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
false
true
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
# Application.get_env
#end
user
else
user
end
end
end