What Ithibati adds to the account schema an application already owns.
defmodule MyApp.Accounts.User do
use Ecto.Schema
alias Ithibati.Schema.Identifier
alias Ithibati.Schema.User
use User, identifier: :email, format: Identifier.email_format()
import Ecto.Changeset
schema "users" do
ithibati_account()
field :name, :string
timestamps(type: :utc_datetime_usec)
end
def changeset(user, attrs) do
user
|> identifier_changeset(attrs)
|> cast(attrs, [:name])
end
endOptions
identifier:— required, a literal atom: the field an account is known by. You have to write this one out here, because it is the field your schema declares.format:— optional, a regular expression, evaluated once when your module compiles.Ithibati.Schema.Identifier.email_format/0offers one for addresses.constraint_name:— optional, the name of the unique index on that column. Say it when your naming convention is not the one Ecto derives. Ithibati then creates the index under that name, and the changeset's constraint matches it.unique_index: false— optional, an opt-out. Say it when your application creates that index itself, and Ithibati checks that one exists rather than creating it.
You may write each of the three value options inline, or name a module attribute standing above
the use line. Ithibati refuses an option written as nil, because that is what a misspelled
attribute looks like.
identifier_changeset/2 trims and lowercases the values it writes, whatever the field is
called.
There is no default, and the macro says so when you leave it out: this library never sends
mail, so it will not ask you for an address by assumption.
Ithibati.Schema.Identifier.email_format/0 says what the offered email pattern accepts and
why it is not RFC 5322.
What it injects, and what it refuses
The macro injects one field, three associations, and three functions: identifier_changeset/2,
passkey_display_name/1 (overridable, nil by default) and __ithibati__/1.
Ithibati.Schema.UserTest pins that list against a schema that does not use this macro, so a
fourth one has to be a decision.
It refuses two things, both at compile time: a module that never calls ithibati_account/0
inside its schema block, and one that defines identifier_changeset/2 or __ithibati__/1
itself.
Summary
Functions
Whether a module carries what this macro injects.
The name and displayName a WebAuthn registration shows, for an account or for an identifier
that does not have one yet.
Whether the identifier is what a failed insert collided on.
Declares the identifier field and the associations Ithibati needs. Call it inside your
schema block, in a module that has use Ithibati.Schema.User above it.
Functions
Whether a module carries what this macro injects.
Ithibati offers one predicate rather than two spellings of it. The marker function has been renamed once already, and a second caller checking it by hand would be a second thing to find by grep next time.
The name and displayName a WebAuthn registration shows, for an account or for an identifier
that does not have one yet.
Ithibati derives both here rather than leaving them to the caller, because it owns the fallback:
an application's passkey_display_name/1 may answer nil and be right. The first registration
on an instance has no account at all, which is why the second clause exists.
Whether the identifier is what a failed insert collided on.
A transaction that refuses an account hands back an Ecto.Changeset, and the application then
has to decide what to tell somebody. "That name is taken" comes from a unique index and "that is
not a name" comes from the format. Only Ithibati knows which field is the identifier, because
ithibati_account/0 declared it.
The match is on the field, so a uniqueness error on it counts however the index is shaped. An
application that scopes the identifier to a tenant gets true for a collision inside that
tenant, which is what a collision means there.
The function answers which of the two it was and nothing more. The application turns that into
:username_taken, a sentence or an HTTP status, the same way Ithibati.Web.Handler leaves it
what a verified assertion is worth.
Only after the database has seen it
A changeset carries a constraint error only after the repo has attempted the insert and been
refused. A changeset built and never given to the repo answers false however certainly the
identifier is taken, and that looks exactly like this function not working.
Declares the identifier field and the associations Ithibati needs. Call it inside your
schema block, in a module that has use Ithibati.Schema.User above it.