Current section
Files
Jump to
Current section
Files
lib/imagine/accounts/user.ex
defmodule Imagine.Accounts.User do
@moduledoc """
User - for authentication, authorization, auditing
"""
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :username, :string
field :current_password, :string, virtual: true
field :password, :string, virtual: true
field :password_hash, :string
field :password_hash_type, :string
field :first_name, :string
field :last_name, :string
field :email, :string
field :dynamic_fields, :string
field :active, :boolean, default: false
field :is_superuser, :boolean, default: false
timestamps(inserted_at: :created_on, updated_at: :updated_on)
end
def name(user) do
names = [to_string(user.first_name), to_string(user.last_name)]
case names do
["", ""] -> user.email
_ -> Enum.join(names, " ")
end
end
@doc false
def changeset(user, attrs) do
case to_string(attrs["password"] || attrs[:password]) do
"" ->
user
|> cast(attrs, [:username, :first_name, :last_name, :email, :active])
|> validate_required([:username, :email, :active])
_ ->
changeset_with_password(user, attrs)
end
end
def changeset_with_password(user, attrs) do
user
|> cast(attrs, [:username, :password, :first_name, :last_name, :email, :active])
|> validate_required([:username, :password, :email, :active])
|> validate_length(:password, min: 8)
|> validate_confirmation(:password)
|> put_pass_hash()
end
def change_password_changeset(user, attrs) do
user
|> cast(attrs, [:current_password, :password])
|> validate_required([:current_password, :password])
|> validate_length(:password, min: 8)
|> validate_confirmation(:password)
|> validate_current_password(user)
|> put_pass_hash()
end
# def registration_changeset(user, attrs) do
# user
# |> cast(attrs, [:username, :password, :first_name, :last_name, :email])
# |> put_change(:active, true)
# |> validate_length(:password, min: 8)
# |> validate_confirmation(:password)
# |> put_pass_hash()
# end
@doc false
def is_superuser_changeset(user, attrs) do
user
|> cast(attrs, [:is_superuser])
|> validate_required([:is_superuser])
end
defp validate_current_password(
%Ecto.Changeset{valid?: true, changes: %{current_password: password}} = changeset,
user
) do
case check_password(user, password) do
true -> changeset
false -> add_error(changeset, :current_password, "is not correct")
end
end
defp validate_current_password(changeset, _user), do: changeset
defp put_pass_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
%{password_hash: hash} = Argon2.add_hash(password)
changeset
|> put_change(:password_hash, hash)
|> put_change(:password_hash_type, "argon2")
end
defp put_pass_hash(changeset), do: changeset
# works with new argon2 hashes and legacy sha1 hashes (and others can be added)
# every legacy installation should have used the default 16-char salt
# ... but if not, we'll find out pretty quick
def check_password(%__MODULE__{} = user, password, saltlen \\ 16) do
case user.password_hash_type do
"argon2" ->
Argon2.verify_pass(password, user.password_hash)
_ ->
# legacy hash
salt = String.slice(user.password_hash, 0..(saltlen - 1))
legacy_sha1_hash(password, salt) == user.password_hash
end
end
def legacy_sha1_hash(password, salt) do
hash = :crypto.hash(:sha, salt <> password) |> Base.encode16() |> String.downcase()
salt <> hash
end
end