Features: - User registration and authentication with email/password - Admin login with username-based authentication (separate from regular users) - Review system for contractors to rate clients - Star rating system with review forms - Client identification with private data protection - Contractor registration with document verification - Admin dashboard for review management - Contact form (demo, non-functional) - Responsive navigation with DaisyUI components - Docker Compose setup for production deployment - PostgreSQL database with Ecto migrations - High Vis color scheme (dark background with safety orange/green) Admin credentials: username: admin, password: admin123 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
84 lines
2.2 KiB
Elixir
84 lines
2.2 KiB
Elixir
defmodule MyFirstElixirVibeCode.Accounts.User do
|
|
use Ecto.Schema
|
|
import Ecto.Changeset
|
|
|
|
schema "users" do
|
|
field :email, :string
|
|
field :username, :string
|
|
field :password, :string, virtual: true, redact: true
|
|
field :password_hash, :string
|
|
field :is_admin, :boolean, default: false
|
|
|
|
timestamps(type: :utc_datetime)
|
|
end
|
|
|
|
@doc false
|
|
def changeset(user, attrs) do
|
|
user
|
|
|> cast(attrs, [:email, :username, :password, :is_admin])
|
|
|> validate_required([:password])
|
|
|> validate_email_or_username()
|
|
|> validate_password()
|
|
|> put_password_hash()
|
|
end
|
|
|
|
defp validate_email_or_username(changeset) do
|
|
is_admin = get_field(changeset, :is_admin)
|
|
|
|
changeset
|
|
|> then(fn cs ->
|
|
if is_admin do
|
|
cs
|
|
|> validate_required([:username])
|
|
|> validate_length(:username, min: 3, max: 20)
|
|
|> unsafe_validate_unique(:username, MyFirstElixirVibeCode.Repo)
|
|
|> unique_constraint(:username)
|
|
else
|
|
cs
|
|
|> validate_required([:email])
|
|
|> validate_email()
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp validate_email(changeset) do
|
|
changeset
|
|
|> 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, MyFirstElixirVibeCode.Repo)
|
|
|> unique_constraint(:email)
|
|
end
|
|
|
|
defp validate_password(changeset) do
|
|
changeset
|
|
|> validate_required([:password])
|
|
|> validate_length(:password, min: 6, max: 72)
|
|
end
|
|
|
|
defp put_password_hash(changeset) do
|
|
case changeset do
|
|
%Ecto.Changeset{valid?: true, changes: %{password: password}} ->
|
|
change(changeset, password_hash: Bcrypt.hash_pwd_salt(password))
|
|
|
|
_ ->
|
|
changeset
|
|
end
|
|
end
|
|
|
|
@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?(%__MODULE__{password_hash: password_hash}, password)
|
|
when is_binary(password_hash) and byte_size(password) > 0 do
|
|
Bcrypt.verify_pass(password, password_hash)
|
|
end
|
|
|
|
def valid_password?(_, _) do
|
|
Bcrypt.no_user_verify()
|
|
false
|
|
end
|
|
end
|