mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-27 13:47:58 +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,
|
||||
repo: Philomena.Repo,
|
||||
web_module: PhilomenaWeb,
|
||||
extensions: [PowResetPassword, PowPersistentSession, PowMultiFactor],
|
||||
extensions: [PowResetPassword, PowPersistentSession],
|
||||
controller_callbacks: Pow.Extension.Phoenix.ControllerCallbacks
|
||||
|
||||
config :bcrypt_elixir,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
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
|
||||
= 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.
|
||||
|
|
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 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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue