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
|
||||
alias RecycledCloud.Repo
|
||||
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
||||
alias RecycledCloud.LDAP
|
||||
|
||||
## Database getters
|
||||
|
||||
@doc """
|
||||
Gets a user by email.
|
||||
Gets a user by username.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user_by_email("foo@example.com")
|
||||
iex> get_user_by_username("foo")
|
||||
%User{}
|
||||
|
||||
iex> get_user_by_email("unknown@example.com")
|
||||
iex> get_user_by_username("unknown")
|
||||
nil
|
||||
|
||||
"""
|
||||
def get_user_by_email(email) when is_binary(email) do
|
||||
Repo.get_by(User, email: email)
|
||||
def get_user_by_username(username) when is_binary(username) do
|
||||
User.get_by_username(username)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a user by email and password.
|
||||
Gets a user by username and password.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_user_by_email_and_password("foo@example.com", "correct_password")
|
||||
iex> get_user_by_username_and_password("foo", "correct_password")
|
||||
%User{}
|
||||
|
||||
iex> get_user_by_email_and_password("foo@example.com", "invalid_password")
|
||||
iex> get_user_by_username_and_password("foo", "invalid_password")
|
||||
nil
|
||||
|
||||
"""
|
||||
def get_user_by_email_and_password(email, password)
|
||||
when is_binary(email) and is_binary(password) do
|
||||
user = Repo.get_by(User, email: email)
|
||||
def get_user_by_username_and_password(username, password)
|
||||
when is_binary(username) and is_binary(password) do
|
||||
user = get_user_by_username(username)
|
||||
if User.valid_password?(user, password), do: user
|
||||
end
|
||||
|
||||
|
@ -180,7 +181,7 @@ defmodule RecycledCloud.Accounts do
|
|||
|
||||
"""
|
||||
def change_user_password(user, attrs \\ %{}) do
|
||||
User.password_changeset(user, attrs, hash_password: false)
|
||||
User.password_changeset(user, attrs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -195,19 +196,24 @@ defmodule RecycledCloud.Accounts do
|
|||
{:error, %Ecto.Changeset{}}
|
||||
|
||||
"""
|
||||
def update_user_password(user, password, attrs) do
|
||||
changeset =
|
||||
user
|
||||
def update_user_password(user, current_password, attrs) do
|
||||
changeset = user
|
||||
|> User.password_changeset(attrs)
|
||||
|> User.validate_current_password(password)
|
||||
|
||||
Ecto.Multi.new()
|
||||
|> Ecto.Multi.update(:user, changeset)
|
||||
|> Ecto.Multi.delete_all(:tokens, UserToken.user_and_contexts_query(user, :all))
|
||||
|> Repo.transaction()
|
||||
|> case do
|
||||
{:ok, %{user: user}} -> {:ok, user}
|
||||
{:error, :user, changeset, _} -> {:error, changeset}
|
||||
|> User.validate_current_password(current_password)
|
||||
case Ecto.Changeset.apply_action(changeset, :update) do
|
||||
{:ok, _} ->
|
||||
new_password = attrs["password"]
|
||||
case User.set_password(user, new_password) do
|
||||
{:ok, _} -> {:ok, user}
|
||||
{:error, err} ->
|
||||
changeset_errors = [current_password: {"Unknown error.", []}]
|
||||
updated_changeset = changeset
|
||||
|> Map.put(:action, :update)
|
||||
|> Map.put(:errors, changeset_errors)
|
||||
{:error, updated_changeset}
|
||||
end
|
||||
err ->
|
||||
err
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,17 +1,63 @@
|
|||
defmodule RecycledCloud.Accounts.User do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
require Logger
|
||||
require Exldap
|
||||
alias RecycledCloud.{LDAP,Accounts}
|
||||
alias RecycledCloud.Accounts.{User, UserToken, UserNotifier}
|
||||
alias RecycledCloud.Repo
|
||||
|
||||
@derive {Inspect, except: [:password]}
|
||||
schema "users" do
|
||||
field :email, :string
|
||||
field :username, :string
|
||||
field :password, :string, virtual: true
|
||||
field :hashed_password, :string
|
||||
field :email, :string, virtual: true
|
||||
field :confirmed_at, :naive_datetime
|
||||
|
||||
timestamps()
|
||||
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 """
|
||||
A user changeset for registration.
|
||||
|
||||
|
@ -31,9 +77,9 @@ defmodule RecycledCloud.Accounts.User do
|
|||
"""
|
||||
def registration_changeset(user, attrs, opts \\ []) do
|
||||
user
|
||||
|> cast(attrs, [:email, :password])
|
||||
|> cast(attrs, [:username, :password])
|
||||
|> validate_email()
|
||||
|> validate_password(opts)
|
||||
#|> validate_password(opts)
|
||||
end
|
||||
|
||||
defp validate_email(changeset) do
|
||||
|
@ -41,31 +87,15 @@ defmodule RecycledCloud.Accounts.User do
|
|||
|> validate_required([:email])
|
||||
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|
||||
|> validate_length(:email, max: 160)
|
||||
|> unsafe_validate_unique(:email, RecycledCloud.Repo)
|
||||
|> unique_constraint(:email)
|
||||
end
|
||||
|
||||
defp validate_password(changeset, opts) do
|
||||
defp validate_password(changeset) do
|
||||
changeset
|
||||
|> 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 upper case 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
|
||||
|
||||
@doc """
|
||||
|
@ -95,11 +125,11 @@ defmodule RecycledCloud.Accounts.User do
|
|||
validations on a LiveView form), this option can be set to `false`.
|
||||
Defaults to `true`.
|
||||
"""
|
||||
def password_changeset(user, attrs, opts \\ []) do
|
||||
def password_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:password])
|
||||
|> validate_confirmation(:password, message: "does not match password")
|
||||
|> validate_password(opts)
|
||||
|> validate_password()
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -112,17 +142,21 @@ defmodule RecycledCloud.Accounts.User do
|
|||
|
||||
@doc """
|
||||
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)
|
||||
when is_binary(hashed_password) and byte_size(password) > 0 do
|
||||
Bcrypt.verify_pass(password, hashed_password)
|
||||
def valid_password?(user = %User{username: uid}, password)
|
||||
when byte_size(password) > 0 do
|
||||
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
|
||||
|
||||
def valid_password?(_, _) do
|
||||
Bcrypt.no_user_verify()
|
||||
false
|
||||
end
|
||||
|
||||
|
@ -136,4 +170,21 @@ defmodule RecycledCloud.Accounts.User do
|
|||
add_error(changeset, :current_password, "is not valid")
|
||||
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
|
||||
|
|
|
@ -14,7 +14,9 @@ defmodule RecycledCloud.Application do
|
|||
# Start the PubSub system
|
||||
{Phoenix.PubSub, name: RecycledCloud.PubSub},
|
||||
# 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)
|
||||
# {RecycledCloud.Worker, arg}
|
||||
]
|
||||
|
|
|
@ -7,24 +7,31 @@ defmodule RecycledCloudWeb.UserRegistrationController do
|
|||
|
||||
def new(conn, _params) do
|
||||
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
|
||||
|
||||
def create(conn, %{"user" => user_params}) do
|
||||
case Accounts.register_user(user_params) do
|
||||
{:ok, user} ->
|
||||
{:ok, _} =
|
||||
Accounts.deliver_user_confirmation_instructions(
|
||||
user,
|
||||
&Routes.user_confirmation_url(conn, :confirm, &1)
|
||||
)
|
||||
changeset = Accounts.change_user_registration(%User{})
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "User created successfully.")
|
||||
|> UserAuth.log_in_user(user)
|
||||
|> redirect(to: Routes.user_registration_path(conn, :new))
|
||||
|
||||
{:error, %Ecto.Changeset{} = changeset} ->
|
||||
render(conn, "new.html", changeset: changeset)
|
||||
end
|
||||
#case Accounts.register_user(user_params) do
|
||||
# {:ok, user} ->
|
||||
# {: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
|
||||
|
|
|
@ -9,8 +9,8 @@ defmodule RecycledCloudWeb.UserResetPasswordController do
|
|||
render(conn, "new.html")
|
||||
end
|
||||
|
||||
def create(conn, %{"user" => %{"email" => email}}) do
|
||||
if user = Accounts.get_user_by_email(email) do
|
||||
def create(conn, %{"user" => %{"username" => username}}) do
|
||||
if user = Accounts.get_user_by_username(username) do
|
||||
Accounts.deliver_user_reset_password_instructions(
|
||||
user,
|
||||
&Routes.user_reset_password_url(conn, :edit, &1)
|
||||
|
|
|
@ -9,12 +9,12 @@ defmodule RecycledCloudWeb.UserSessionController do
|
|||
end
|
||||
|
||||
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)
|
||||
else
|
||||
render(conn, "new.html", error_message: "Invalid email or password")
|
||||
render(conn, "new.html", error_message: "Invalid username or password")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<li><%= link "Home", to: Routes.page_path(@conn, :index) %></li>
|
||||
|
||||
<%= 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 "Log out", to: Routes.user_session_path(@conn, :delete), method: :delete %></li>
|
||||
<% else %>
|
||||
|
|
|
@ -12,8 +12,14 @@
|
|||
<div class="row">
|
||||
<div class="col-2" id="section-nav">
|
||||
<%= 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 class="col-10">
|
||||
<div class="col-6">
|
||||
<main role="main" class="container-fluid">
|
||||
<%= if get_flash(@conn, :info) do %>
|
||||
<p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
|
||||
|
|
|
@ -7,20 +7,17 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= label f, :email %>
|
||||
<%= email_input f, :email, required: true %>
|
||||
<%= error_tag f, :email %>
|
||||
<%= text_input f, :username, required: true, placeholder: "Username" %>
|
||||
<%= error_tag f, :username %>
|
||||
|
||||
<%= label f, :password %>
|
||||
<%= password_input f, :password, required: true %>
|
||||
<br />
|
||||
|
||||
<%= password_input f, :password, required: true, placeholder: "Password" %>
|
||||
<%= error_tag f, :password %>
|
||||
|
||||
<br />
|
||||
|
||||
<div>
|
||||
<%= submit "Register" %>
|
||||
</div>
|
||||
<% 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>
|
||||
|
||||
<%= form_for :user, Routes.user_reset_password_path(@conn, :create), fn f -> %>
|
||||
<%= label f, :email %>
|
||||
<%= email_input f, :email, required: true %>
|
||||
<%= text_input f, :username, required: true, placeholder: "Username" %>
|
||||
|
||||
<div>
|
||||
<%= submit "Send instructions to reset password" %>
|
||||
|
|
|
@ -11,7 +11,7 @@ if you do not already have one.</p>
|
|||
</div>
|
||||
<% 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 />
|
||||
<%= label f, :remember_me, "Keep me logged in for 60 days" %>
|
||||
<%= 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>
|
||||
|
||||
|
@ -11,12 +16,12 @@
|
|||
|
||||
<%= hidden_input f, :action, name: "action", value: "update_email" %>
|
||||
|
||||
<%= label f, :email %>
|
||||
<%= email_input f, :email, required: true %>
|
||||
<%= email_input f, :email, required: true, placeholder: "Email" %>
|
||||
<%= error_tag f, :email %>
|
||||
|
||||
<%= label f, :current_password, for: "current_password_for_email" %>
|
||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email" %>
|
||||
<br />
|
||||
|
||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_email", placeholder: "Current password" %>
|
||||
<%= error_tag f, :current_password %>
|
||||
|
||||
<div>
|
||||
|
@ -35,16 +40,17 @@
|
|||
|
||||
<%= hidden_input f, :action, name: "action", value: "update_password" %>
|
||||
|
||||
<%= label f, :password, "New password" %>
|
||||
<%= password_input f, :password, required: true %>
|
||||
<%= password_input f, :password, required: true, placeholder: "New password" %>
|
||||
<%= error_tag f, :password %>
|
||||
|
||||
<%= label f, :password_confirmation, "Confirm new password" %>
|
||||
<%= password_input f, :password_confirmation, required: true %>
|
||||
<br />
|
||||
|
||||
<%= password_input f, :password_confirmation, required: true, placeholder: "New password confirmation" %>
|
||||
<%= error_tag f, :password_confirmation %>
|
||||
|
||||
<%= label f, :current_password, for: "current_password_for_password" %>
|
||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password" %>
|
||||
<br />
|
||||
|
||||
<%= password_input f, :current_password, required: true, name: "current_password", id: "current_password_for_password", placeholder: "Current password" %>
|
||||
<%= error_tag f, :current_password %>
|
||||
|
||||
<div>
|
||||
|
|
1
mix.exs
1
mix.exs
|
@ -46,6 +46,7 @@ defmodule RecycledCloud.MixProject do
|
|||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.0"},
|
||||
{:phx_gen_auth, "~> 0.6", only: [:dev], runtime: false},
|
||||
{:exldap, git: "https://github.com/Fnux/exldap.git"}
|
||||
]
|
||||
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_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"},
|
||||
"exldap": {:git, "https://github.com/Fnux/exldap.git", "fd4b5c9928d2e6c4d4ba0fef95f9456ec8b088b9", []},
|
||||
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
|
||||
"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"},
|
||||
|
|
|
@ -5,13 +5,12 @@ defmodule RecycledCloud.Repo.Migrations.CreateUsersAuthTables do
|
|||
execute "CREATE EXTENSION IF NOT EXISTS citext", ""
|
||||
|
||||
create table(:users) do
|
||||
add :email, :citext, null: false
|
||||
add :hashed_password, :string, null: false
|
||||
add :username, :citext, null: false
|
||||
add :confirmed_at, :naive_datetime
|
||||
timestamps()
|
||||
end
|
||||
|
||||
create unique_index(:users, [:email])
|
||||
create unique_index(:users, [:username])
|
||||
|
||||
create table(:users_tokens) do
|
||||
add :user_id, references(:users, on_delete: :delete_all), null: false
|
||||
|
|
Loading…
Reference in a new issue