authentication: initial LDAP wiring (login, password change)
This commit is contained in:
parent
f286e83f6e
commit
d96d9005f5
16 changed files with 264 additions and 108 deletions
81
lib/recycledcloud/LDAP.ex
Normal file
81
lib/recycledcloud/LDAP.ex
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
defmodule RecycledCloud.LDAP do
|
||||||
|
require Logger
|
||||||
|
require Exldap
|
||||||
|
use GenServer
|
||||||
|
|
||||||
|
# Every request to the LDAP backend go through this GenServer.
|
||||||
|
|
||||||
|
def start_link(_) do
|
||||||
|
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def init(_) do
|
||||||
|
{:ok, connect()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:pool, query}, _from, state) do
|
||||||
|
case state do
|
||||||
|
%{status: :ok, conn: ldap_conn} ->
|
||||||
|
result = internal_execute_query(query, ldap_conn)
|
||||||
|
if (result == {:error, :ldap_closed}) do
|
||||||
|
{:reply, result, connect()}
|
||||||
|
else
|
||||||
|
{:reply, result, state}
|
||||||
|
end
|
||||||
|
%{status: :error, conn: nil} ->
|
||||||
|
state = connect()
|
||||||
|
if (state.status == :ok) do
|
||||||
|
result = internal_execute_query(query, state.conn)
|
||||||
|
{:reply, result, state}
|
||||||
|
else
|
||||||
|
error = {:error, "Unable to contact backend"}
|
||||||
|
{:reply, error, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:single, query}, _from, state) do
|
||||||
|
single_state = connect()
|
||||||
|
case single_state do
|
||||||
|
%{status: :ok, conn: ldap_conn} ->
|
||||||
|
result = internal_execute_query(query, ldap_conn)
|
||||||
|
close(ldap_conn)
|
||||||
|
{:reply, result, state}
|
||||||
|
%{status: :error, conn: nil} ->
|
||||||
|
error = {:error, "Unable to contact backend"}
|
||||||
|
{:reply, error, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def terminate(reason, state) do
|
||||||
|
if (state.status == :ok), do: close(state.conn)
|
||||||
|
IO.inspect(reason)
|
||||||
|
#Logger.info "Terminating LDAP backend: #{reason}."
|
||||||
|
end
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
def execute(query), do: GenServer.call(__MODULE__, {:pool, query})
|
||||||
|
def execute_single(query), do: GenServer.call(__MODULE__, {:single, query})
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
defp internal_execute_query(query, ldap_conn), do: query.(ldap_conn)
|
||||||
|
|
||||||
|
defp connect do
|
||||||
|
case Exldap.connect do
|
||||||
|
{:ok, ldap_conn} ->
|
||||||
|
Logger.info "Successfuly connected to LDAP server."
|
||||||
|
%{status: :ok, conn: ldap_conn}
|
||||||
|
{:error, err} ->
|
||||||
|
Logger.warning "Failed to connect to LDAP server: #{err}. Authentication will not be possible."
|
||||||
|
%{status: :error, conn: nil}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp close(ldap_conn) do
|
||||||
|
ldap_conn |> Exldap.close
|
||||||
|
Logger.debug("An LDAP connection was closed.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
@ -6,40 +6,41 @@ defmodule RecycledCloud.Accounts do
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias RecycledCloud.Repo
|
alias RecycledCloud.Repo
|
||||||
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
||||||
|
alias RecycledCloud.LDAP
|
||||||
|
|
||||||
## Database getters
|
## Database getters
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a user by email.
|
Gets a user by username.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> get_user_by_email("foo@example.com")
|
iex> get_user_by_username("foo")
|
||||||
%User{}
|
%User{}
|
||||||
|
|
||||||
iex> get_user_by_email("unknown@example.com")
|
iex> get_user_by_username("unknown")
|
||||||
nil
|
nil
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_user_by_email(email) when is_binary(email) do
|
def get_user_by_username(username) when is_binary(username) do
|
||||||
Repo.get_by(User, email: email)
|
User.get_by_username(username)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a user by email and password.
|
Gets a user by username and password.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
|
iex> get_user_by_username_and_password("foo", "correct_password")
|
||||||
%User{}
|
%User{}
|
||||||
|
|
||||||
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
|
iex> get_user_by_username_and_password("foo", "invalid_password")
|
||||||
nil
|
nil
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def get_user_by_email_and_password(email, password)
|
def get_user_by_username_and_password(username, password)
|
||||||
when is_binary(email) and is_binary(password) do
|
when is_binary(username) and is_binary(password) do
|
||||||
user = Repo.get_by(User, email: email)
|
user = get_user_by_username(username)
|
||||||
if User.valid_password?(user, password), do: user
|
if User.valid_password?(user, password), do: user
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -180,7 +181,7 @@ defmodule RecycledCloud.Accounts do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def change_user_password(user, attrs \\ %{}) do
|
def change_user_password(user, attrs \\ %{}) do
|
||||||
User.password_changeset(user, attrs, hash_password: false)
|
User.password_changeset(user, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -195,19 +196,24 @@ defmodule RecycledCloud.Accounts do
|
||||||
{:error, %Ecto.Changeset{}}
|
{:error, %Ecto.Changeset{}}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def update_user_password(user, password, attrs) do
|
def update_user_password(user, current_password, attrs) do
|
||||||
changeset =
|
changeset = user
|
||||||
user
|
|
||||||
|> User.password_changeset(attrs)
|
|> User.password_changeset(attrs)
|
||||||
|> User.validate_current_password(password)
|
|> User.validate_current_password(current_password)
|
||||||
|
case Ecto.Changeset.apply_action(changeset, :update) do
|
||||||
Ecto.Multi.new()
|
{:ok, _} ->
|
||||||
|> Ecto.Multi.update(:user, changeset)
|
new_password = attrs["password"]
|
||||||
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
|
case User.set_password(user, new_password) do
|
||||||
|> Repo.transaction()
|
{:ok, _} -> {:ok, user}
|
||||||
|> case do
|
{:error, err} ->
|
||||||
{:ok, %{user: user}} -> {:ok, user}
|
changeset_errors = [current_password: {"Unknown error.", []}]
|
||||||
{:error, :user, changeset, _} -> {:error, changeset}
|
updated_changeset = changeset
|
||||||
|
|> Map.put(:action, :update)
|
||||||
|
|> Map.put(:errors, changeset_errors)
|
||||||
|
{:error, updated_changeset}
|
||||||
|
end
|
||||||
|
err ->
|
||||||
|
err
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,63 @@
|
||||||
defmodule RecycledCloud.Accounts.User do
|
defmodule RecycledCloud.Accounts.User do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
require Logger
|
||||||
|
require Exldap
|
||||||
|
alias RecycledCloud.{LDAP,Accounts}
|
||||||
|
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
||||||
|
alias RecycledCloud.Repo
|
||||||
|
|
||||||
@derive {Inspect, except: [:password]}
|
@derive {Inspect, except: [:password]}
|
||||||
schema "users" do
|
schema "users" do
|
||||||
field :email, :string
|
field :username, :string
|
||||||
field :password, :string, virtual: true
|
field :password, :string, virtual: true
|
||||||
field :hashed_password, :string
|
field :email, :string, virtual: true
|
||||||
field :confirmed_at, :naive_datetime
|
field :confirmed_at, :naive_datetime
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp get_dn_for(%User{username: uid}) do
|
||||||
|
# FIXME
|
||||||
|
"uid=#{uid},ou=users,dc=recycled,dc=cloud"
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_by_username(username) when is_binary(username) do
|
||||||
|
local_user = Repo.get_by(User, username: username)
|
||||||
|
if local_user do
|
||||||
|
Map.put(local_user, :email, "unknown@domain.tld")
|
||||||
|
else
|
||||||
|
query = fn ldap_conn -> Exldap.search_field(ldap_conn, :uid, username) end
|
||||||
|
case query |> LDAP.execute do
|
||||||
|
{:ok, []} -> nil
|
||||||
|
{:ok, result} ->
|
||||||
|
{:ok, entry} = result |> Enum.fetch(0)
|
||||||
|
|
||||||
|
Logger.info("Found #{entry.object_name} in directory. Syncing with \
|
||||||
|
local database.")
|
||||||
|
|
||||||
|
attributes = entry |> Map.get(:attributes) |> Enum.into(%{})
|
||||||
|
username = attributes
|
||||||
|
|> Map.get('uid')
|
||||||
|
|> Enum.at(0)
|
||||||
|
|> List.to_string
|
||||||
|
|
||||||
|
email = attributes
|
||||||
|
|> Map.get('email')
|
||||||
|
|> Enum.at(0)
|
||||||
|
|> List.to_string
|
||||||
|
|
||||||
|
case Accounts.register_user(%{username: username, email: email}) do
|
||||||
|
{:ok, user} -> user
|
||||||
|
{:error, _} -> nil
|
||||||
|
end
|
||||||
|
{:error, err} ->
|
||||||
|
Logger.warn("LDAP error: #{err}")
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
A user changeset for registration.
|
A user changeset for registration.
|
||||||
|
|
||||||
|
@ -31,9 +77,9 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
"""
|
"""
|
||||||
def registration_changeset(user, attrs, opts \\ []) do
|
def registration_changeset(user, attrs, opts \\ []) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:email, :password])
|
|> cast(attrs, [:username, :password])
|
||||||
|> validate_email()
|
|> validate_email()
|
||||||
|> validate_password(opts)
|
#|> validate_password(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_email(changeset) do
|
defp validate_email(changeset) do
|
||||||
|
@ -41,31 +87,15 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
|> validate_required([:email])
|
|> validate_required([:email])
|
||||||
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|
||||||
|> validate_length(:email, max: 160)
|
|> validate_length(:email, max: 160)
|
||||||
|> unsafe_validate_unique(:email, RecycledCloud.Repo)
|
|
||||||
|> unique_constraint(:email)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_password(changeset, opts) do
|
defp validate_password(changeset) do
|
||||||
changeset
|
changeset
|
||||||
|> validate_required([:password])
|
|> validate_required([:password])
|
||||||
|> validate_length(:password, min: 12, max: 80)
|
|> validate_length(:password, min: 6, max: 80)
|
||||||
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
#|> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
|
||||||
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
#|> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
|
||||||
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
#|> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|
||||||
|> maybe_hash_password(opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_hash_password(changeset, opts) do
|
|
||||||
hash_password? = Keyword.get(opts, :hash_password, true)
|
|
||||||
password = get_change(changeset, :password)
|
|
||||||
|
|
||||||
if hash_password? && password && changeset.valid? do
|
|
||||||
changeset
|
|
||||||
|> put_change(:hashed_password, Bcrypt.hash_pwd_salt(password))
|
|
||||||
|> delete_change(:password)
|
|
||||||
else
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -95,11 +125,11 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
validations on a LiveView form), this option can be set to `false`.
|
validations on a LiveView form), this option can be set to `false`.
|
||||||
Defaults to `true`.
|
Defaults to `true`.
|
||||||
"""
|
"""
|
||||||
def password_changeset(user, attrs, opts \\ []) do
|
def password_changeset(user, attrs) do
|
||||||
user
|
user
|
||||||
|> cast(attrs, [:password])
|
|> cast(attrs, [:password])
|
||||||
|> validate_confirmation(:password, message: "does not match password")
|
|> validate_confirmation(:password, message: "does not match password")
|
||||||
|> validate_password(opts)
|
|> validate_password()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -112,17 +142,21 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Verifies the password.
|
Verifies the password.
|
||||||
|
|
||||||
If there is no user or the user doesn't have a password, we call
|
|
||||||
`Bcrypt.no_user_verify/0` to avoid timing attacks.
|
|
||||||
"""
|
"""
|
||||||
def valid_password?(%RecycledCloud.Accounts.User{hashed_password: hashed_password}, password)
|
def valid_password?(user = %User{username: uid}, password)
|
||||||
when is_binary(hashed_password) and byte_size(password) > 0 do
|
when byte_size(password) > 0 do
|
||||||
Bcrypt.verify_pass(password, hashed_password)
|
dn = get_dn_for(user)
|
||||||
|
query = fn ldap_conn ->
|
||||||
|
Exldap.verify_credentials(ldap_conn, dn, password)
|
||||||
|
end
|
||||||
|
|
||||||
|
case query |> LDAP.execute_single do
|
||||||
|
:ok -> true
|
||||||
|
{:error, _} -> false
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def valid_password?(_, _) do
|
def valid_password?(_, _) do
|
||||||
Bcrypt.no_user_verify()
|
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -136,4 +170,21 @@ defmodule RecycledCloud.Accounts.User do
|
||||||
add_error(changeset, :current_password, "is not valid")
|
add_error(changeset, :current_password, "is not valid")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def set_password(user, new_password) do
|
||||||
|
# Exldap does not properly implement `change_password`, hence we have to
|
||||||
|
# fallback on erlang's `:eldap`.
|
||||||
|
# See http://erlang.org/doc/man/eldap.html#modify-4
|
||||||
|
|
||||||
|
user_dn = get_dn_for(user)
|
||||||
|
query = fn ldap_conn ->
|
||||||
|
:eldap.modify_password(
|
||||||
|
ldap_conn,
|
||||||
|
String.to_charlist(user_dn),
|
||||||
|
String.to_charlist(new_password)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
query |> LDAP.execute_single
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,7 +14,9 @@ defmodule RecycledCloud.Application do
|
||||||
# Start the PubSub system
|
# Start the PubSub system
|
||||||
{Phoenix.PubSub, name: RecycledCloud.PubSub},
|
{Phoenix.PubSub, name: RecycledCloud.PubSub},
|
||||||
# Start the Endpoint (http/https)
|
# Start the Endpoint (http/https)
|
||||||
RecycledCloudWeb.Endpoint
|
RecycledCloudWeb.Endpoint,
|
||||||
|
# Starts LDAP connection pool
|
||||||
|
RecycledCloud.LDAP
|
||||||
# Start a worker by calling: RecycledCloud.Worker.start_link(arg)
|
# Start a worker by calling: RecycledCloud.Worker.start_link(arg)
|
||||||
# {RecycledCloud.Worker, arg}
|
# {RecycledCloud.Worker, arg}
|
||||||
]
|
]
|
||||||
|
|
|
@ -7,24 +7,31 @@ defmodule RecycledCloudWeb.UserRegistrationController do
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
changeset = Accounts.change_user_registration(%User{})
|
changeset = Accounts.change_user_registration(%User{})
|
||||||
render(conn, "new.html", changeset: changeset)
|
conn
|
||||||
|
|> put_flash(:error, "Registration is disabled for the time being.")
|
||||||
|
|> render("new.html", changeset: changeset)
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => user_params}) do
|
def create(conn, %{"user" => user_params}) do
|
||||||
case Accounts.register_user(user_params) do
|
changeset = Accounts.change_user_registration(%User{})
|
||||||
{:ok, user} ->
|
|
||||||
{:ok, _} =
|
|
||||||
Accounts.deliver_user_confirmation_instructions(
|
|
||||||
user,
|
|
||||||
&Routes.user_confirmation_url(conn, :confirm, &1)
|
|
||||||
)
|
|
||||||
|
|
||||||
conn
|
conn
|
||||||
|> put_flash(:info, "User created successfully.")
|
|> redirect(to: Routes.user_registration_path(conn, :new))
|
||||||
|> UserAuth.log_in_user(user)
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
#case Accounts.register_user(user_params) do
|
||||||
render(conn, "new.html", changeset: changeset)
|
# {:ok, user} ->
|
||||||
end
|
# {:ok, _} =
|
||||||
|
# Accounts.deliver_user_confirmation_instructions(
|
||||||
|
# user,
|
||||||
|
# &Routes.user_confirmation_url(conn, :confirm, &1)
|
||||||
|
# )
|
||||||
|
|
||||||
|
# conn
|
||||||
|
# |> put_flash(:info, "User created successfully.")
|
||||||
|
# |> UserAuth.log_in_user(user)
|
||||||
|
|
||||||
|
# {:error, %Ecto.Changeset{} = changeset} ->
|
||||||
|
# render(conn, "new.html", changeset: changeset)
|
||||||
|
#end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,8 +9,8 @@ defmodule RecycledCloudWeb.UserResetPasswordController do
|
||||||
render(conn, "new.html")
|
render(conn, "new.html")
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => %{"email" => email}}) do
|
def create(conn, %{"user" => %{"username" => username}}) do
|
||||||
if user = Accounts.get_user_by_email(email) do
|
if user = Accounts.get_user_by_username(username) do
|
||||||
Accounts.deliver_user_reset_password_instructions(
|
Accounts.deliver_user_reset_password_instructions(
|
||||||
user,
|
user,
|
||||||
&Routes.user_reset_password_url(conn, :edit, &1)
|
&Routes.user_reset_password_url(conn, :edit, &1)
|
||||||
|
|
|
@ -9,12 +9,12 @@ defmodule RecycledCloudWeb.UserSessionController do
|
||||||
end
|
end
|
||||||
|
|
||||||
def create(conn, %{"user" => user_params}) do
|
def create(conn, %{"user" => user_params}) do
|
||||||
%{"email" => email, "password" => password} = user_params
|
%{"username" => username, "password" => password} = user_params
|
||||||
|
|
||||||
if user = Accounts.get_user_by_email_and_password(email, password) do
|
if user = Accounts.get_user_by_username_and_password(username, password) do
|
||||||
UserAuth.log_in_user(conn, user, user_params)
|
UserAuth.log_in_user(conn, user, user_params)
|
||||||
else
|
else
|
||||||
render(conn, "new.html", error_message: "Invalid email or password")
|
render(conn, "new.html", error_message: "Invalid username or password")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<li><%= link "Home", to: Routes.page_path(@conn, :index) %></li>
|
<li><%= link "Home", to: Routes.page_path(@conn, :index) %></li>
|
||||||
|
|
||||||
<%= if @current_user do %>
|
<%= if @current_user do %>
|
||||||
<li><%= @current_user.email %></li>
|
<li>Logged in as <%= @current_user.username %></li>
|
||||||
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
|
<li><%= link "Settings", to: Routes.user_settings_path(@conn, :edit) %></li>
|
||||||
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
|
<li><%= link "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
|
||||||
<% else %>
|
<% else %>
|
||||||
|
|
|
@ -12,8 +12,14 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-2" id="section-nav">
|
<div class="col-2" id="section-nav">
|
||||||
<%= render "_sidebar_nav.html", assigns %>
|
<%= render "_sidebar_nav.html", assigns %>
|
||||||
|
<img id="nav-logo" src="<%= Routes.static_path(@conn, "/images/cloud.svg") %>"></img>
|
||||||
|
<p class="nav-notice">
|
||||||
|
<small>
|
||||||
|
management v<%= Mix.Project.config[:version] %> | <%= Mix.env %> | <a href="https://code.recycled.cloud/e-Durable/management">sources</a>
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-10">
|
<div class="col-6">
|
||||||
<main role="main" class="container-fluid">
|
<main role="main" class="container-fluid">
|
||||||
<%= if get_flash(@conn, :info) do %>
|
<%= if get_flash(@conn, :info) do %>
|
||||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||||
|
|
|
@ -7,20 +7,17 @@
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= label f, :email %>
|
<%= text_input f, :username, required: true, placeholder: "Username" %>
|
||||||
<%= email_input f, :email, required: true %>
|
<%= error_tag f, :username %>
|
||||||
<%= error_tag f, :email %>
|
|
||||||
|
|
||||||
<%= label f, :password %>
|
<br />
|
||||||
<%= password_input f, :password, required: true %>
|
|
||||||
|
<%= password_input f, :password, required: true, placeholder: "Password" %>
|
||||||
<%= error_tag f, :password %>
|
<%= error_tag f, :password %>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= submit "Register" %>
|
<%= submit "Register" %>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<p>
|
|
||||||
<%= link "Log in", to: Routes.user_session_path(@conn, :new) %> |
|
|
||||||
<%= link "Forgot your password?", to: Routes.user_reset_password_path(@conn, :new) %>
|
|
||||||
</p>
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<h1>Forgot your password?</h1>
|
<h1>Forgot your password?</h1>
|
||||||
|
|
||||||
<%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %>
|
<%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %>
|
||||||
<%= label f, :email %>
|
<%= text_input f, :username, required: true, placeholder: "Username" %>
|
||||||
<%= email_input f, :email, required: true %>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<%= submit "Send instructions to reset password" %>
|
<%= submit "Send instructions to reset password" %>
|
||||||
|
|
|
@ -11,7 +11,7 @@ if you do not already have one.</p>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
<%= email_input f, :email, required: true, placeholder: "Email" %> <br />
|
<%= text_input f, :username, required: true, placeholder: "Username" %> <br />
|
||||||
<%= password_input f, :password, required: true, placeholder: "Password" %> <br />
|
<%= password_input f, :password, required: true, placeholder: "Password" %> <br />
|
||||||
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
|
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
|
||||||
<%= checkbox f, :remember_me %>
|
<%= checkbox f, :remember_me %>
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
<h1>Settings</h1>
|
<h1>Account settings</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
You are currently logged in as <i><%= @current_user.username %></i>, using <%
|
||||||
|
@current_user.email %> as primary contact method.
|
||||||
|
</p>
|
||||||
|
|
||||||
<h3>Change email</h3>
|
<h3>Change email</h3>
|
||||||
|
|
||||||
|
@ -11,12 +16,12 @@
|
||||||
|
|
||||||
<%= hidden_input f, :action, name: "action", value: "update_email" %>
|
<%= hidden_input f, :action, name: "action", value: "update_email" %>
|
||||||
|
|
||||||
<%= label f, :email %>
|
<%= email_input f, :email, required: true, placeholder: "Email" %>
|
||||||
<%= email_input f, :email, required: true %>
|
|
||||||
<%= error_tag f, :email %>
|
<%= error_tag f, :email %>
|
||||||
|
|
||||||
<%= label f, :current_password, for: "current_password_for_email" %>
|
<br />
|
||||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %>
|
|
||||||
|
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email", placeholder: "Current password" %>
|
||||||
<%= error_tag f, :current_password %>
|
<%= error_tag f, :current_password %>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
@ -35,16 +40,17 @@
|
||||||
|
|
||||||
<%= hidden_input f, :action, name: "action", value: "update_password" %>
|
<%= hidden_input f, :action, name: "action", value: "update_password" %>
|
||||||
|
|
||||||
<%= label f, :password, "New password" %>
|
<%= password_input f, :password, required: true, placeholder: "New password" %>
|
||||||
<%= password_input f, :password, required: true %>
|
|
||||||
<%= error_tag f, :password %>
|
<%= error_tag f, :password %>
|
||||||
|
|
||||||
<%= label f, :password_confirmation, "Confirm new password" %>
|
<br />
|
||||||
<%= password_input f, :password_confirmation, required: true %>
|
|
||||||
|
<%= password_input f, :password_confirmation, required: true, placeholder: "New password confirmation" %>
|
||||||
<%= error_tag f, :password_confirmation %>
|
<%= error_tag f, :password_confirmation %>
|
||||||
|
|
||||||
<%= label f, :current_password, for: "current_password_for_password" %>
|
<br />
|
||||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
|
|
||||||
|
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password", placeholder: "Current password" %>
|
||||||
<%= error_tag f, :current_password %>
|
<%= error_tag f, :current_password %>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
1
mix.exs
1
mix.exs
|
@ -46,6 +46,7 @@ defmodule RecycledCloud.MixProject do
|
||||||
{:jason, "~> 1.0"},
|
{:jason, "~> 1.0"},
|
||||||
{:plug_cowboy, "~> 2.0"},
|
{:plug_cowboy, "~> 2.0"},
|
||||||
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
|
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
|
||||||
|
{:exldap, git: "https://github.com/Fnux/exldap.git"}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -10,6 +10,7 @@
|
||||||
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
|
"ecto": {:hex, :ecto, "3.5.5", "48219a991bb86daba6e38a1e64f8cea540cded58950ff38fbc8163e062281a07", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "98dd0e5e1de7f45beca6130d13116eae675db59adfa055fb79612406acf6f6f1"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
|
"ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"},
|
||||||
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
"elixir_make": {:hex, :elixir_make, "0.6.2", "7dffacd77dec4c37b39af867cedaabb0b59f6a871f89722c25b28fcd4bd70530", [:mix], [], "hexpm", "03e49eadda22526a7e5279d53321d1cced6552f344ba4e03e619063de75348d9"},
|
||||||
|
"exldap": {:git, "https://github.com/Fnux/exldap.git", "fd4b5c9928d2e6c4d4ba0fef95f9456ec8b088b9", []},
|
||||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
||||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||||
|
|
|
@ -5,13 +5,12 @@ defmodule RecycledCloud.Repo.Migrations.CreateUsersAuthTables do
|
||||||
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
|
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
|
||||||
|
|
||||||
create table(:users) do
|
create table(:users) do
|
||||||
add :email, :citext, null: false
|
add :username, :citext, null: false
|
||||||
add :hashed_password, :string, null: false
|
|
||||||
add :confirmed_at, :naive_datetime
|
add :confirmed_at, :naive_datetime
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
create unique_index(:users, [:email])
|
create unique_index(:users, [:username])
|
||||||
|
|
||||||
create table(:users_tokens) do
|
create table(:users_tokens) do
|
||||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||||
|
|
Loading…
Reference in a new issue