ratemyclient/lib/my_first_elixir_vibe_code/accounts/user.ex
Kevin Sivic 9ece312442 Initial commit: RateMyClient™ Phoenix application
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>
2025-11-28 21:30:50 -05:00

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