add captcha, controller, images
1
.iex.exs
|
@ -1,2 +1,3 @@
|
|||
alias Philomena.{Repo, Comments.Comment, Posts.Post, Images.Image, Tags.Tag, Users.User}
|
||||
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,
|
||||
# Starts a worker by calling: Philomena.Worker.start_link(arg)
|
||||
# {Philomena.Worker, arg},
|
||||
Pow.Store.Backend.MnesiaCache
|
||||
Pow.Store.Backend.MnesiaCache,
|
||||
{Redix, name: :redix}
|
||||
]
|
||||
|
||||
# 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
|
||||
resources "/filters", FilterController
|
||||
resources "/profiles", ProfileController, only: [:show]
|
||||
resources "/captchas", CaptchaController, only: [:create]
|
||||
|
||||
get "/:id", ImageController, :show
|
||||
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
|
||||
= 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"
|
||||
|
||||
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"},
|
||||
{:scrivener_ecto, "~> 2.0"},
|
||||
{:pbkdf2, "~> 2.0"},
|
||||
{:qrcode, "~> 0.1.5"}
|
||||
{:qrcode, "~> 0.1.5"},
|
||||
{:redix, "~> 0.10.2"}
|
||||
]
|
||||
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"},
|
||||
"qrcode": {:hex, :qrcode, "0.1.5", "551271830515c150f34568345b060c625deb0e6691db2a01b0a6de3aafc93886", [:mix], [], "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"},
|
||||
"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"},
|
||||
|
|