Ithibati.Identity.Invitations (Ithibati v0.1.0)

Copy Markdown View Source

Opening an invitation, and accepting one.

The invitation itself is the application's row, and Ithibati.Schema.Invitation says why. This module holds the half that would otherwise be written by hand and written slightly wrong: finding an invitation by the secret in a link, and marking it accepted in a way two people opening that link at once cannot both get through.

Accepting composes into the caller's own transaction, beside the grant:

Ecto.Multi.new()
|> Ecto.Multi.insert(:account, MyApp.Accounts.User.changeset(%User{}, account_attrs(invitation)))
|> Ithibati.Identity.Invitations.accept(invitation)
|> Ithibati.Identity.Grant.with_key_and_codes(key_attrs)
|> Ecto.Multi.insert(:membership, fn %{account: account, invitation: invitation} ->  end)
|> MyApp.Repo.transaction()

Accept before the grant, for the reason Ithibati.Identity.Grant.with_key_and_codes/3 warns about. Two people opening one link is the race this step's refusal exists for, so it is not a rare path here.

Summary

Functions

Marks the invitation accepted, as a step named :invitation in the caller's transaction.

The attributes an account created from this invitation starts with.

Removes them, in one statement, and answers how many.

Invitations that ran out without being accepted.

The pending invitation this token opens, or nil.

Functions

accept(multi, invitation, opts \\ [])

Marks the invitation accepted, as a step named :invitation in the caller's transaction.

The step answers {:error, :invalid_invitation} when the invitation was accepted or expired in the meantime, and that rolls the whole transaction back. The account, its passkey and its codes go with it.

It also refuses an account being created under a different identifier from the one the invitation was addressed to, with {:error, :identifier_mismatch}. account: names the step that account comes from and defaults to :account, the name every fragment in Ithibati uses. A transaction with no such step is not checked, because there is nothing to check it against.

account_attrs(invitation)

The attributes an account created from this invitation starts with.

Only the identifier, under the name the schemas agreed on. An application composing the acceptance therefore does not have to reach into Ithibati to learn what that name is.

delete_expired()

Removes them, in one statement, and answers how many.

expired()

Invitations that ran out without being accepted.

Ithibati does not sweep them. Scheduling a job is the application's business, and fetch/1 refuses an expired invitation anyway, so one left lying is inert. The query is Ithibati's, though, which is why both halves are offered. Use this one for a sweeper that wants to say what it is about to remove, and delete_expired/0 for one that does not.

fetch(token)

The pending invitation this token opens, or nil.

Pending means not yet accepted and not past its expiry. An invitation that is not pending is answered the same way as a token nobody holds, deliberately.