# `Drafter.Accounts`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/accounts.ex#L1)

A file-backed store of user accounts for a served application.

Passwords are kept as PBKDF2-HMAC-SHA256 hashes with a random salt per account;
the file never holds a password. Usernames are unique without regard to case
and are returned in the spelling they were registered with. Each account carries
a `props` map the application owns.

    {:ok, accounts} = Drafter.Accounts.start_link(path: "/var/lib/myapp/accounts.terms")
    :ok = Drafter.Accounts.register(accounts, "alice", "correct horse")
    {:ok, %{username: "alice", props: %{}}} = Drafter.Accounts.authenticate(accounts, "alice", "correct horse")

`authenticate/4` costs the same for an unknown name as for a wrong password, and
refuses a peer outright, without hashing, once it has failed five times in
fifteen minutes.

## Rules

  * a username is 1 to 32 characters from `A-Z`, `a-z`, `0-9`, `_` and `-`
  * a password is at least 8 characters
  * every change is written to the file before the call returns, by writing a
    sibling file and renaming it over the old one
  * the file is text: one Erlang term per account, in registration order, that
    `:file.consult/1` reads — `%{username: "alice", number: 0, props: %{…}, hash:
    <<…>>, salt: <<…>>, iterations: 300000}` — so it can be read and searched as it
    is. It is UTF-8: a binary that is printable text is written as one, any other as
    its bytes. A file written as `:erlang.term_to_binary/1` by an earlier version is
    read and written back as text, as is one an earlier version wrote as Latin-1

# `account`

```elixir
@type account() :: %{username: username(), props: map()}
```

# `register_error`

```elixir
@type register_error() :: :taken | :invalid_username | :weak_password
```

# `username`

```elixir
@type username() :: String.t()
```

# `authenticate`

```elixir
@spec authenticate(GenServer.server(), username(), String.t(), keyword()) ::
  {:ok, account()} | :error | {:error, :locked}
```

Check a username and password.

Returns `{:ok, account}`, `:error` for a wrong password or unknown name, or
`{:error, :locked}` for a peer that has failed too often.

## Options

  * `:peer` - the caller's address, any term, counted by the throttle. Default
    `nil`, which is never locked.

# `change_password`

```elixir
@spec change_password(GenServer.server(), username(), String.t(), String.t()) ::
  :ok | :error | {:error, :weak_password}
```

Replace the password, given the current one. `:error` when it does not match.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `fetch`

```elixir
@spec fetch(GenServer.server(), username()) :: {:ok, account()} | :error
```

The account under `username`, without checking a password.

# `put_props`

```elixir
@spec put_props(GenServer.server(), username(), map()) :: :ok | {:error, :unknown}
```

Merge `props` into the account's props. `{:error, :unknown}` for no such account.

# `register`

```elixir
@spec register(GenServer.server(), username(), String.t(), map()) ::
  :ok | {:error, register_error()}
```

Create an account. Returns `:ok`, or `{:error, reason}` with `:taken`,
`:invalid_username` or `:weak_password`.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start the store, loading `path` if it exists.

## Options

  * `:path` - the file to load and write. Required.
  * `:name` - a GenServer name. Default: none.
  * `:iterations` - PBKDF2 rounds for new hashes. Default `300_000`. An account
    keeps the count it was hashed with, so raising this later affects only new
    passwords.
  * `:throttle` - options for `Drafter.Accounts.Throttle.new/1`. Default `[]`.
  * `:default_props` - a function from an account's number (0 for the first
    registered, then 1, 2, …; accounts from a file without numbers are numbered in
    name order when loaded) to props every account has unless its own props say
    otherwise. Default: none.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
