mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
filter editor
This commit is contained in:
parent
4d7ef4a18d
commit
2c04e1cf3d
14 changed files with 239 additions and 39 deletions
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
56
lib/philomena_web/templates/filter/_form.html.slime
Normal file
56
lib/philomena_web/templates/filter/_form.html.slime
Normal 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"
|
2
lib/philomena_web/templates/filter/edit.html.slime
Normal file
2
lib/philomena_web/templates/filter/edit.html.slime
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
h2 Editing Filter
|
||||||
|
= render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :update, @filter)
|
|
@ -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.
|
||||||
|
|
2
lib/philomena_web/templates/filter/new.html.slime
Normal file
2
lib/philomena_web/templates/filter/new.html.slime
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
h2 Creating New Filter
|
||||||
|
= render PhilomenaWeb.FilterView, "_form.html", filter: @changeset, route: Routes.filter_path(@conn, :create)
|
9
lib/philomena_web/templates/tag/_tag_editor.html.slime
Normal file
9
lib/philomena_web/templates/tag/_tag_editor.html.slime
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue