add captcha, controller, images
3
.iex.exs
|
@ -1,2 +1,3 @@
|
||||||
alias Philomena.{Repo, Comments.Comment, Posts.Post, Images.Image, Tags.Tag, Users.User}
|
alias Philomena.{Repo, Comments.Comment, Posts.Post, Images.Image, Tags.Tag, Users.User}
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
import Ecto.Changeset
|
BIN
assets/static/images/captcha/1.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
assets/static/images/captcha/2.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/static/images/captcha/3.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
assets/static/images/captcha/4.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
assets/static/images/captcha/5.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
assets/static/images/captcha/6.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
assets/static/images/captcha/background.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/static/images/captcha/i1.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
assets/static/images/captcha/i2.png
Normal file
After Width: | Height: | Size: 7.3 KiB |
BIN
assets/static/images/captcha/i3.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
assets/static/images/captcha/i4.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/static/images/captcha/i5.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
BIN
assets/static/images/captcha/i6.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
|
@ -14,7 +14,8 @@ defmodule Philomena.Application do
|
||||||
PhilomenaWeb.Endpoint,
|
PhilomenaWeb.Endpoint,
|
||||||
# Starts a worker by calling: Philomena.Worker.start_link(arg)
|
# Starts a worker by calling: Philomena.Worker.start_link(arg)
|
||||||
# {Philomena.Worker, arg},
|
# {Philomena.Worker, arg},
|
||||||
Pow.Store.Backend.MnesiaCache
|
Pow.Store.Backend.MnesiaCache,
|
||||||
|
{Redix, name: :redix}
|
||||||
]
|
]
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
|
|
130
lib/philomena/captcha.ex
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
defmodule Philomena.Captcha do
|
||||||
|
defstruct [:image_base64, :solution, :solution_id]
|
||||||
|
|
||||||
|
@numbers ~W(1 2 3 4 5 6)
|
||||||
|
@images ~W(1 2 3 4 5 6)
|
||||||
|
@base_path File.cwd!() <> "/assets/static/images/captcha"
|
||||||
|
|
||||||
|
@number_files %{
|
||||||
|
"1" => @base_path <> "/1.png",
|
||||||
|
"2" => @base_path <> "/2.png",
|
||||||
|
"3" => @base_path <> "/3.png",
|
||||||
|
"4" => @base_path <> "/4.png",
|
||||||
|
"5" => @base_path <> "/5.png",
|
||||||
|
"6" => @base_path <> "/6.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
@image_files %{
|
||||||
|
"1" => @base_path <> "/i1.png",
|
||||||
|
"2" => @base_path <> "/i2.png",
|
||||||
|
"3" => @base_path <> "/i3.png",
|
||||||
|
"4" => @base_path <> "/i4.png",
|
||||||
|
"5" => @base_path <> "/i5.png",
|
||||||
|
"6" => @base_path <> "/i6.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
@background_file @base_path <> "/background.png"
|
||||||
|
|
||||||
|
@geometry %{
|
||||||
|
1 => "+0+0", 2 => "+120+0", 3 => "+240+0",
|
||||||
|
4 => "+0+120", 5 => "+120+120", 6 => "+240+120",
|
||||||
|
7 => "+0+240", 8 => "+120+240", 9 => "+240+240"
|
||||||
|
}
|
||||||
|
|
||||||
|
@distortion_1 [
|
||||||
|
~W"-implode .1",
|
||||||
|
~W"-implode -.1"
|
||||||
|
]
|
||||||
|
|
||||||
|
@distortion_2 [
|
||||||
|
~W"-swirl 10",
|
||||||
|
~W"-swirl -10",
|
||||||
|
~W"-swirl 20",
|
||||||
|
~W"-swirl -20"
|
||||||
|
]
|
||||||
|
|
||||||
|
@distortion_3 [
|
||||||
|
~W"-wave 5x180",
|
||||||
|
~W"-wave 5x126",
|
||||||
|
~W"-wave 10x180",
|
||||||
|
~W"-wave 10x126"
|
||||||
|
]
|
||||||
|
|
||||||
|
def create do
|
||||||
|
solution =
|
||||||
|
Enum.zip(@numbers, Enum.shuffle(@images))
|
||||||
|
|> Map.new()
|
||||||
|
|
||||||
|
# 3x3 render grid
|
||||||
|
grid = Enum.shuffle(@numbers ++ [nil, nil, nil])
|
||||||
|
|
||||||
|
# Base arguments
|
||||||
|
args = [
|
||||||
|
"-page", "360x360",
|
||||||
|
@background_file
|
||||||
|
]
|
||||||
|
|
||||||
|
# Individual grid files
|
||||||
|
files =
|
||||||
|
grid
|
||||||
|
|> Enum.with_index()
|
||||||
|
|> Enum.flat_map(fn {num, index} ->
|
||||||
|
if num do
|
||||||
|
[
|
||||||
|
"(", @image_files[solution[num]], ")", "-geometry", @geometry[index + 1], "-composite",
|
||||||
|
"(", @number_files[num], ")", "-geometry", @geometry[index + 1], "-composite"
|
||||||
|
]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Distortions for more unpredictability
|
||||||
|
distortions =
|
||||||
|
[
|
||||||
|
Enum.random(@distortion_1),
|
||||||
|
Enum.random(@distortion_2),
|
||||||
|
Enum.random(@distortion_3)
|
||||||
|
]
|
||||||
|
|> Enum.shuffle()
|
||||||
|
|> List.flatten()
|
||||||
|
|
||||||
|
jpeg = ~W"-quality 8 jpeg:-"
|
||||||
|
|
||||||
|
{image, 0} = System.cmd("convert", args ++ files ++ distortions ++ jpeg)
|
||||||
|
image = image |> Base.encode64()
|
||||||
|
|
||||||
|
# Store solution in redis to prevent reuse
|
||||||
|
# Solutions are valid for 10 minutes
|
||||||
|
solution_id =
|
||||||
|
:crypto.strong_rand_bytes(12)
|
||||||
|
|> Base.encode16(case: :lower)
|
||||||
|
solution_id = "cp_" <> solution_id
|
||||||
|
|
||||||
|
{:ok, _ok} = Redix.command(:redix, ["SET", solution_id, Jason.encode!(solution)])
|
||||||
|
{:ok, _ok} = Redix.command(:redix, ["EXPIRE", solution_id, 600])
|
||||||
|
|
||||||
|
%Philomena.Captcha{
|
||||||
|
image_base64: image,
|
||||||
|
solution: solution,
|
||||||
|
solution_id: solution_id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_solution?(<<"cp_", _rest::binary>> = solution_id, solution) do
|
||||||
|
# Delete key immediately. This may race, but should
|
||||||
|
# have minimal impact if the race succeeds.
|
||||||
|
with {:ok, sol} <- Redix.command(:redix, ["GET", solution_id]),
|
||||||
|
{:ok, _del} <- Redix.command(:redix, ["DEL", solution_id]),
|
||||||
|
{:ok, sol} <- Jason.decode(to_string(sol))
|
||||||
|
do
|
||||||
|
Map.equal?(solution, sol)
|
||||||
|
else
|
||||||
|
_ ->
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_solution?(_solution_id, _solution),
|
||||||
|
do: false
|
||||||
|
end
|
11
lib/philomena_web/controllers/captcha_controller.ex
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
defmodule PhilomenaWeb.CaptchaController do
|
||||||
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
|
alias Philomena.Captcha
|
||||||
|
|
||||||
|
def create(conn, _params) do
|
||||||
|
captcha = Captcha.create()
|
||||||
|
|
||||||
|
render(conn, "create.html", captcha: captcha, layout: false)
|
||||||
|
end
|
||||||
|
end
|
|
@ -67,6 +67,7 @@ defmodule PhilomenaWeb.Router do
|
||||||
end
|
end
|
||||||
resources "/filters", FilterController
|
resources "/filters", FilterController
|
||||||
resources "/profiles", ProfileController, only: [:show]
|
resources "/profiles", ProfileController, only: [:show]
|
||||||
|
resources "/captchas", CaptchaController, only: [:create]
|
||||||
|
|
||||||
get "/:id", ImageController, :show
|
get "/:id", ImageController, :show
|
||||||
end
|
end
|
||||||
|
|
20
lib/philomena_web/templates/captcha/create.html.slime
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
elixir:
|
||||||
|
options = [
|
||||||
|
"Applejack": 1,
|
||||||
|
"Fluttershy": 2,
|
||||||
|
"Pinkie Pie": 3,
|
||||||
|
"Rainbow Dash": 4,
|
||||||
|
"Rarity": 5,
|
||||||
|
"Twilight Sparkle": 6
|
||||||
|
]
|
||||||
|
|
||||||
|
div
|
||||||
|
= hidden_input :captcha, :id, value: @captcha.solution_id
|
||||||
|
img src="data:image/jpeg;base64,#{@captcha.image_base64}"
|
||||||
|
|
||||||
|
= for i <- (1..6) do
|
||||||
|
.field
|
||||||
|
label> for="captcha_sln[#{i}]"
|
||||||
|
| Name of pony with cutie mark #
|
||||||
|
= i
|
||||||
|
= select :captcha, "sln[#{i}]", options, class: "input"
|
|
@ -19,6 +19,10 @@ h1 Sign in
|
||||||
= checkbox f, :persistent_session
|
= checkbox f, :persistent_session
|
||||||
= label f, :persistent_session, "Remember me"
|
= label f, :persistent_session, "Remember me"
|
||||||
|
|
||||||
|
.field
|
||||||
|
= checkbox f, :captcha, class: "js-captcha", value: 0
|
||||||
|
= label f, :captcha, "I am not a robot!"
|
||||||
|
|
||||||
= submit "Sign in", class: "button"
|
= submit "Sign in", class: "button"
|
||||||
|
|
||||||
p
|
p
|
||||||
|
|
3
lib/philomena_web/views/captcha_view.ex
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defmodule PhilomenaWeb.CaptchaView do
|
||||||
|
use PhilomenaWeb, :view
|
||||||
|
end
|
3
mix.exs
|
@ -54,7 +54,8 @@ defmodule Philomena.MixProject do
|
||||||
{:canary, "~> 1.1.1"},
|
{:canary, "~> 1.1.1"},
|
||||||
{:scrivener_ecto, "~> 2.0"},
|
{:scrivener_ecto, "~> 2.0"},
|
||||||
{:pbkdf2, "~> 2.0"},
|
{:pbkdf2, "~> 2.0"},
|
||||||
{:qrcode, "~> 0.1.5"}
|
{:qrcode, "~> 0.1.5"},
|
||||||
|
{:redix, "~> 0.10.2"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
mix.lock
|
@ -43,6 +43,7 @@
|
||||||
"pow": {:hex, :pow, "1.0.13", "5ca3e8d9fecca037bfb0ea3b8dde070cc319746498e844d59fc209d461b0d426", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3.0 or ~> 1.4.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
|
"pow": {:hex, :pow, "1.0.13", "5ca3e8d9fecca037bfb0ea3b8dde070cc319746498e844d59fc209d461b0d426", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.3.0 or ~> 1.4.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 2.0.0 and <= 3.0.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"},
|
||||||
"qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm"},
|
"qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "hexpm"},
|
||||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
|
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
|
||||||
|
"redix": {:hex, :redix, "0.10.2", "a9eabf47898aa878650df36194aeb63966d74f5bd69d9caa37babb32dbb93c5d", [:mix], [{:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
"retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"},
|
"retry": {:hex, :retry, "0.13.0", "bb9b2713f70f39337837852337ad280c77662574f4fb852a8386c269f3d734c4", [:mix], [], "hexpm"},
|
||||||
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},
|
"scrivener": {:hex, :scrivener, "2.7.0", "fa94cdea21fad0649921d8066b1833d18d296217bfdf4a5389a2f45ee857b773", [:mix], [], "hexpm"},
|
||||||
"scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"},
|
"scrivener_ecto": {:hex, :scrivener_ecto, "2.2.0", "53d5f1ba28f35f17891cf526ee102f8f225b7024d1cdaf8984875467158c9c5e", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm"},
|
||||||
|
|