Commit 556d5341320b4c052056104ca3b21f462c3eedc8

Thomas de Grivel 2022-01-13T11:14:02

totp

diff --git a/lib/kmxgit/user_manager.ex b/lib/kmxgit/user_manager.ex
index cef4b22..c3829a7 100644
--- a/lib/kmxgit/user_manager.ex
+++ b/lib/kmxgit/user_manager.ex
@@ -236,8 +236,8 @@ defmodule Kmxgit.UserManager do
     |> Repo.delete()
   end
 
-  def change_user(%User{} = user \\ %User{}) do
-    User.changeset(user, %{})
+  def change_user(%User{} = user \\ %User{}, params \\ %{}) do
+    User.changeset(user, params)
   end
 
   def authenticate_user(login, password) do
@@ -259,10 +259,10 @@ defmodule Kmxgit.UserManager do
     end
   end
 
-  def otp_init do
+  def totp_init do
     Repo.transaction fn ->
       Enum.each list_users(), fn u ->
-        {:ok, _} = User.otp_changeset(u) |> Repo.update()
+        {:ok, _} = User.totp_changeset(u) |> Repo.update()
       end
     end
   end
@@ -274,7 +274,7 @@ defmodule Kmxgit.UserManager do
   ## Examples
       iex> generate_totp_enrolment_url(user)
   """
-  def totp_enrolment_url(%User{email: email, otp_secret: secret}) do
+  def totp_enrolment_url(%User{email: email, totp_secret: secret}) do
     "otpauth://totp/kmxgit:#{email}?secret=#{secret}&issuer=kmxgit&algorithm=SHA1&digits=6&period=30"
   end
 
@@ -285,7 +285,7 @@ defmodule Kmxgit.UserManager do
   end
 
   def verify_user_totp(user = %User{}, token) do
-    User.totp_verify(user, token)
+    User.totp_verify(user, token || 0)
   end
 
   def delete_user_totp(user = %User{}) do
diff --git a/lib/kmxgit/user_manager/user.ex b/lib/kmxgit/user_manager/user.ex
index bf99abb..41af245 100644
--- a/lib/kmxgit/user_manager/user.ex
+++ b/lib/kmxgit/user_manager/user.ex
@@ -15,8 +15,8 @@ defmodule Kmxgit.UserManager.User do
     field :hashed_password, :string, redact: true
     field :is_admin, :boolean, null: false, default: false
     field :name, :string
-    field :otp_last, :integer, default: 0, redact: true
-    field :otp_secret, :string, redact: true
+    field :totp_last, :integer, default: 0, redact: true
+    field :totp_secret, :string, redact: true
     has_many :owned_repositories, Repository
     field :password, :string, virtual: true, redact: true
     field :password_confirmation, :string, virtual: true, redact: true
@@ -47,16 +47,16 @@ defmodule Kmxgit.UserManager.User do
   def registration_changeset(user, attrs, opts \\ []) do
     user
     |> cast(attrs, [:email, :password])
-    |> generate_otp_secret()
+    |> generate_totp_secret()
     |> validate_email()
     |> validate_password(opts)
     |> common_changeset()
   end
 
-  def otp_changeset(user) do
+  def totp_changeset(user) do
     user
     |> cast(%{}, [])
-    |> generate_otp_secret()
+    |> generate_totp_secret()
     |> common_changeset()
   end
 
@@ -177,15 +177,15 @@ defmodule Kmxgit.UserManager.User do
   defp common_changeset(changeset) do
     changeset
     |> cast_assoc(:slug)
-    |> validate_required([:deploy_only, :email, :hashed_password, :is_admin, :otp_secret, :slug])
+    |> validate_required([:deploy_only, :email, :hashed_password, :is_admin, :totp_secret, :slug])
     |> validate_email()
     |> Markdown.validate_markdown(:description)
     |> foreign_key_constraint(:owned_repositories, name: :repositories_user_id_fkey)
   end
 
-  defp generate_otp_secret(changeset) do
+  defp generate_totp_secret(changeset) do
     secret = :crypto.strong_rand_bytes(10) |> Base.encode32()
-    put_change(changeset, :otp_secret, secret)
+    put_change(changeset, :totp_secret, secret)
   end
 
   def changeset(user, attrs \\ %{}) do
@@ -205,7 +205,7 @@ defmodule Kmxgit.UserManager.User do
   def admin_create_user_changeset(user, attrs \\ %{}, opts \\ []) do
     user
     |> cast(attrs, [:deploy_only, :description, :email, :is_admin, :name, :password, :ssh_keys])
-    |> generate_otp_secret()
+    |> generate_totp_secret()
     |> validate_email()
     |> maybe_validate_password(opts)
     |> common_changeset()
@@ -246,27 +246,27 @@ defmodule Kmxgit.UserManager.User do
     end
   end
 
-  def totp_verify(%__MODULE__{otp_secret: secret}, token) do
+  def totp_verify(%__MODULE__{totp_secret: secret}, token) do
     :pot.valid_totp(token, secret, [window: 1, addwindow: 1])
   end
 
   def totp_changeset(user, :delete) do
     user
-    |> cast(%{otp_last: 0}, [:otp_last])
+    |> cast(%{totp_last: 0}, [:totp_last])
   end
   def totp_changeset(user, params) do
     user
-    |> cast(params, [:otp_last])
-    |> verify_otp_last()
+    |> cast(params, [:totp_last])
+    |> verify_totp_last()
   end
 
-  defp verify_otp_last(changeset) do
-    otp_last = Integer.to_string get_field(changeset, :otp_last)
+  defp verify_totp_last(changeset) do
+    otp_last = changeset |> get_field(:otp_last) |> Integer.to_string()
     if totp_verify(changeset.data, otp_last) do
       changeset
     else
       changeset
-      |> add_error(:otp_last, "invalid token")
+      |> add_error(:totp_last, "invalid token")
     end
   end
 end
diff --git a/lib/kmxgit_web/controllers/repository_controller.ex b/lib/kmxgit_web/controllers/repository_controller.ex
index 6704ab0..bacf007 100644
--- a/lib/kmxgit_web/controllers/repository_controller.ex
+++ b/lib/kmxgit_web/controllers/repository_controller.ex
@@ -281,11 +281,12 @@ defmodule KmxgitWeb.RepositoryController do
 
   defp git_put_log1(git, repo, branch, path) do
     slug = Repository.full_slug(repo)
-    {:ok, log1} = if path do
-      GitManager.log1_file(slug, path, branch)
-    else
-      GitManager.log1(slug, branch)
-    end
+    log1 = case if path, do: GitManager.log1_file(slug, path, branch), else: GitManager.log1(slug, branch) do
+             {:ok, log1} -> log1
+             {:error, err} ->
+               IO.inspect(err)
+               nil
+           end
     %{git | log1: log1}
   end
 
diff --git a/lib/kmxgit_web/controllers/user_controller.ex b/lib/kmxgit_web/controllers/user_controller.ex
index 2d29c97..b6d36ed 100644
--- a/lib/kmxgit_web/controllers/user_controller.ex
+++ b/lib/kmxgit_web/controllers/user_controller.ex
@@ -79,7 +79,7 @@ defmodule KmxgitWeb.UserController do
     if params["login"] == User.login(current_user) do
       user = current_user
       changeset = UserManager.change_user(user)
-      |> Ecto.Changeset.put_change(:otp_last, "")
+      |> Ecto.Changeset.put_change(:totp_last, "")
       totp_enrolment_qrcode_src = totp_enrolment_qrcode_src(user)
       conn
       |> assign(:changeset, changeset)
diff --git a/lib/kmxgit_web/controllers/user_session_controller.ex b/lib/kmxgit_web/controllers/user_session_controller.ex
index 6cf2b9c..7713644 100644
--- a/lib/kmxgit_web/controllers/user_session_controller.ex
+++ b/lib/kmxgit_web/controllers/user_session_controller.ex
@@ -2,21 +2,34 @@ defmodule KmxgitWeb.UserSessionController do
   use KmxgitWeb, :controller
 
   alias Kmxgit.UserManager
+  alias Kmxgit.UserManager.User
   alias KmxgitWeb.UserAuth
 
   def new(conn, _params) do
     render(conn, "new.html", error_message: nil)
   end
 
-  def create(conn, %{"user" => user_params}) do
-    %{"login" => login, "password" => password, "otp" => otp} = user_params
-    if user = UserManager.get_user_by_login_and_password(login, password) do
-      if user.otp_last == 0 || UserManager.verify_user_totp(user, otp) do
+  def create(conn, params = %{"user" => user_params}) do
+    user_id = conn |> get_session(:check_totp_for)
+    user = if user_id do
+      {id, ""} = Integer.parse(user_id)
+      UserManager.get_user(id)
+    else
+      %{"login" => login, "password" => password} = user_params
+      UserManager.get_user_by_login_and_password(login, password)
+    end
+    totp = user_params["totp"]
+    if user do
+      if user.totp_last == 0 || totp && UserManager.verify_user_totp(user, totp) do
         UserAuth.log_in_user(conn, user, user_params)
       else
+        changeset = UserManager.change_user(%User{}, user_params)
         conn
+        |> put_session(:check_totp_for, Integer.to_string(user.id))
+        |> assign(:changeset, changeset)
         |> assign(:error_message, "Invalid token")
-        |> render("new.html")
+        |> assign(:totp, totp)
+        |> render("totp.html")
       end
     else
       # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
diff --git a/lib/kmxgit_web/templates/page/user_agreement.html.heex b/lib/kmxgit_web/templates/page/user_agreement.html.heex
index 7072bcc..e258761 100644
--- a/lib/kmxgit_web/templates/page/user_agreement.html.heex
+++ b/lib/kmxgit_web/templates/page/user_agreement.html.heex
@@ -2,7 +2,9 @@
 
   <h1>User agreement</h1>
 
-  Please upload only legal source code and assets.
+  Please upload only legal source code and assets for which you have the
+  copyright or written permission of the author.
+
 
   <h2>Violations</h2>
 
diff --git a/lib/kmxgit_web/templates/user/edit.html.heex b/lib/kmxgit_web/templates/user/edit.html.heex
index 7928466..4e6ba30 100644
--- a/lib/kmxgit_web/templates/user/edit.html.heex
+++ b/lib/kmxgit_web/templates/user/edit.html.heex
@@ -119,7 +119,7 @@
   <hr/>
   <h2><%= gettext "Two factor authentification (2FA)" %></h2>
 
-  <%= if @user.otp_last != 0 do %>
+  <%= if @user.totp_last != 0 do %>
     <p>
       <%= gettext "2FA enabled (TOTP)" %>
     </p>
diff --git a/lib/kmxgit_web/templates/user_session/new.html.heex b/lib/kmxgit_web/templates/user_session/new.html.heex
index aaa1edb..c49ae2a 100644
--- a/lib/kmxgit_web/templates/user_session/new.html.heex
+++ b/lib/kmxgit_web/templates/user_session/new.html.heex
@@ -20,12 +20,6 @@
       <%= error_tag f, :password %>
     </div>
 
-    <div class="mb-3">
-      <%= label f, :otp, gettext("TOTP (Google Authenticator)"), class: "form-label" %>
-      <%= number_input f, :otp, class: "form-control" %>
-      <%= error_tag f, :otp %>
-    </div>
-
     <div class="mb-3 form-check">
       <%= checkbox f, :remember_me, class: "form-check-input" %>
       <%= label f, :remember_me, "Keep me logged in for 60 days", class: "form-check-label" %>
diff --git a/lib/kmxgit_web/templates/user_session/totp.html.heex b/lib/kmxgit_web/templates/user_session/totp.html.heex
new file mode 100644
index 0000000..875ce39
--- /dev/null
+++ b/lib/kmxgit_web/templates/user_session/totp.html.heex
@@ -0,0 +1,28 @@
+<div class="container-fluid">
+  <h1><%= gettext "Log in (2FA)" %></h1>
+
+  <.form let={f} for={@changeset} action={Routes.user_session_path(@conn, :create)} as={:user}>
+    <%= if @totp do %>
+      <div class="alert alert-danger">
+        <p><%= @error_message %></p>
+      </div>
+    <% end %>
+
+    <div class="mb-3">
+      <%= label f, :totp, gettext("TOTP (Google Authenticator)"), class: "form-label" %>
+      <%= number_input f, :totp, class: "form-control" %>
+      <%= error_tag f, :totp %>
+    </div>
+
+    <div class="mb-3 form-check">
+      <%= checkbox f, :remember_me, class: "form-check-input" %>
+      <%= label f, :remember_me, "Keep me logged in for 60 days", class: "form-check-label" %>
+    </div>
+
+    <%= render "recaptcha.html", assigns %>
+
+    <div>
+      <%= submit gettext("Submit"), class: "btn btn-primary" %>
+    </div>
+  </.form>
+</div>
diff --git a/mix.exs b/mix.exs
index 6547332..b193804 100644
--- a/mix.exs
+++ b/mix.exs
@@ -34,11 +34,9 @@ defmodule Kmxgit.MixProject do
   defp deps do
     [
       {:bcrypt_elixir, "~> 2.0"},
-      {:dart_sass, "~> 0.2", runtime: Mix.env() == :dev},
       {:earmark, "~> 1.4.5"},
       {:ecto_sql, "~> 3.6"},
       {:elixir_auth_google, "~> 1.6.2"},
-      {:esbuild, "~> 0.2", runtime: Mix.env() == :dev},
       {:floki, ">= 0.30.0", only: :test},
       {:gen_smtp, "~> 1.1"},
       {:gettext, "~> 0.18"},
diff --git a/priv/repo/migrations/20220111151254_change_otp_to_totp.exs b/priv/repo/migrations/20220111151254_change_otp_to_totp.exs
new file mode 100644
index 0000000..cdadb44
--- /dev/null
+++ b/priv/repo/migrations/20220111151254_change_otp_to_totp.exs
@@ -0,0 +1,8 @@
+defmodule Kmxgit.Repo.Migrations.ChangeOtpToTotp do
+  use Ecto.Migration
+
+  def change do
+    rename table(:users), :otp_last, to: :totp_last
+    rename table(:users), :otp_secret, to: :totp_secret
+  end
+end