improvment!: Update to support Ash 3.0.

This commit is contained in:
James Harton 2024-03-25 13:41:40 +13:00
parent 71d510efc6
commit a9fd9a68f7
Signed by: james
GPG key ID: 90E82DAA13F624F4
93 changed files with 1200 additions and 625 deletions

View file

@ -1,7 +1,6 @@
spark_locals_without_parens = [ spark_locals_without_parens = [
access_token_attribute_name: 1, access_token_attribute_name: 1,
access_token_expires_at_attribute_name: 1, access_token_expires_at_attribute_name: 1,
api: 1,
auth0: 0, auth0: 0,
auth0: 1, auth0: 1,
auth0: 2, auth0: 2,
@ -21,6 +20,7 @@ spark_locals_without_parens = [
confirmation_required?: 1, confirmation_required?: 1,
confirmed_at_field: 1, confirmed_at_field: 1,
destroy_action_name: 1, destroy_action_name: 1,
domain: 1,
enabled?: 1, enabled?: 1,
expunge_expired_action_name: 1, expunge_expired_action_name: 1,
expunge_interval: 1, expunge_interval: 1,
@ -106,7 +106,8 @@ spark_locals_without_parens = [
] ]
[ [
import_deps: [:ash, :spark, :ash_json_api, :ash_graphql], # , :ash_json_api, :ash_graphql],
import_deps: [:ash, :spark],
inputs: [ inputs: [
"*.{ex,exs}", "*.{ex,exs}",
"{dev,config,lib,test}/**/*.{ex,exs}" "{dev,config,lib,test}/**/*.{ex,exs}"

View file

@ -13,7 +13,7 @@ config :git_ops,
config :ash_authentication, DevServer, start?: true, port: 4000 config :ash_authentication, DevServer, start?: true, port: 4000
config :ash_authentication, ecto_repos: [Example.Repo], ash_apis: [Example] config :ash_authentication, ecto_repos: [Example.Repo], ash_domains: [Example]
config :ash_authentication, Example.Repo, config :ash_authentication, Example.Repo,
username: "postgres", username: "postgres",

View file

@ -1,6 +1,6 @@
import Config import Config
config :ash_authentication, ecto_repos: [Example.Repo], ash_apis: [Example] config :ash_authentication, ecto_repos: [Example.Repo], ash_domains: [Example]
config :ash_authentication, Example.Repo, config :ash_authentication, Example.Repo,
username: "postgres", username: "postgres",

View file

@ -1,14 +1,14 @@
defmodule DevServer.ApiRouter do # defmodule DevServer.ApiRouter do
@moduledoc """ # @moduledoc """
Router for API Requests. # Router for API Requests.
""" # """
use Plug.Router # use Plug.Router
import Example.AuthPlug # import Example.AuthPlug
plug(:load_from_bearer) # plug(:load_from_bearer)
plug(:set_actor, :user) # plug(:set_actor, :user)
plug(:match) # plug(:match)
plug(:dispatch) # plug(:dispatch)
forward("/", to: DevServer.JsonApiRouter) # forward("/", to: DevServer.JsonApiRouter)
end # end

View file

@ -1,26 +1,26 @@
defmodule DevServer.GqlRouter do # defmodule DevServer.GqlRouter do
@moduledoc """ # @moduledoc """
Router for GraphQL requests. # Router for GraphQL requests.
""" # """
use Plug.Router # use Plug.Router
import Example.AuthPlug # import Example.AuthPlug
plug(:load_from_bearer) # plug(:load_from_bearer)
plug(:set_actor, :user) # plug(:set_actor, :user)
plug(AshGraphql.Plug) # plug(AshGraphql.Plug)
plug(:match) # plug(:match)
plug(:dispatch) # plug(:dispatch)
forward("/playground", # forward("/playground",
to: Absinthe.Plug.GraphiQL, # to: Absinthe.Plug.GraphiQL,
init_opts: [ # init_opts: [
schema: Example.Schema, # schema: Example.Schema,
interface: :playground # interface: :playground
] # ]
) # )
forward("/", # forward("/",
to: Absinthe.Plug, # to: Absinthe.Plug,
init_opts: [schema: Example.Schema] # init_opts: [schema: Example.Schema]
) # )
end # end

View file

@ -1,4 +1,4 @@
defmodule DevServer.JsonApiRouter do # defmodule DevServer.JsonApiRouter do
@moduledoc false # @moduledoc false
use AshJsonApi.Api.Router, api: Example # use AshJsonApi.Api.Router, api: Example
end # end

View file

@ -13,7 +13,7 @@ defmodule DevServer.Router do
forward("/auth", to: Example.AuthPlug) forward("/auth", to: Example.AuthPlug)
get("/clear_session", to: DevServer.ClearSession) get("/clear_session", to: DevServer.ClearSession)
post("/token_check", to: DevServer.TokenCheck) post("/token_check", to: DevServer.TokenCheck)
forward("/api", to: DevServer.ApiRouter) # forward("/api", to: DevServer.ApiRouter)
forward("/gql", to: DevServer.GqlRouter) # forward("/gql", to: DevServer.GqlRouter)
forward("/", to: DevServer.WebRouter) forward("/", to: DevServer.WebRouter)
end end

View file

@ -10,7 +10,7 @@
<h2>Resources:</h2> <h2>Resources:</h2>
<%= for {resource, options, strategies} <- @resources do %> <%= for {resource, options, strategies} <- @resources do %>
<h2><%= inspect(options.subject_name) %> - <%= Ash.Api.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2> <h2><%= inspect(options.subject_name) %> - <%= Ash.Domain.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2>
<%= for strategy <- strategies do %> <%= for strategy <- strategies do %>

View file

@ -22,7 +22,8 @@ minimum requirements:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -30,8 +31,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
add_ons do add_ons do
confirmation :confirm do confirmation :confirm do
monitor_fields [:email] monitor_fields [:email]

View file

@ -19,7 +19,8 @@ There are other options documented in the DSL.
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -27,8 +28,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
magic_link do magic_link do
identity_field :email identity_field :email

View file

@ -20,7 +20,8 @@ the following minimum criteria:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -28,8 +29,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
client_id "OAuth Client ID" client_id "OAuth Client ID"
@ -158,8 +157,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
registration_enabled? false registration_enabled? false
@ -196,8 +193,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
end end

View file

@ -19,7 +19,8 @@ There are other options documented in the DSL.
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -28,8 +29,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
identity_field :email identity_field :email

View file

@ -30,11 +30,8 @@ system to function.
defmodule MyApp.Accounts.Token do defmodule MyApp.Accounts.Token do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource] extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
token do
api MyApp.Accounts
end
postgres do postgres do
table "tokens" table "tokens"
@ -70,7 +67,7 @@ Configuration options for this token resource
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`api`](#token-api){: #token-api } | `module` | | The Ash API to use to access this resource. | | [`domain`](#token-domain){: #token-domain } | `module` | | The Ash domain to use to access this resource. |
| [`expunge_expired_action_name`](#token-expunge_expired_action_name){: #token-expunge_expired_action_name } | `atom` | `:expunge_expired` | The name of the action used to remove expired tokens. | | [`expunge_expired_action_name`](#token-expunge_expired_action_name){: #token-expunge_expired_action_name } | `atom` | `:expunge_expired` | The name of the action used to remove expired tokens. |
| [`read_expired_action_name`](#token-read_expired_action_name){: #token-read_expired_action_name } | `atom` | `:read_expired` | The name of the action use to find all expired tokens. | | [`read_expired_action_name`](#token-read_expired_action_name){: #token-read_expired_action_name } | `atom` | `:read_expired` | The name of the action use to find all expired tokens. |
| [`expunge_interval`](#token-expunge_interval){: #token-expunge_interval } | `pos_integer` | `12` | How often to scan this resource for records which have expired, and thus can be removed. | | [`expunge_interval`](#token-expunge_interval){: #token-expunge_interval } | `pos_integer` | `12` | How often to scan this resource for records which have expired, and thus can be removed. |

View file

@ -29,10 +29,10 @@ unlikely that you will need to customise it.
defmodule MyApp.Accounts.UserIdentity do defmodule MyApp.Accounts.UserIdentity do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.UserIdentity] extensions: [AshAuthentication.UserIdentity],
domain: MyApp.Accounts
user_identity do user_identity do
api MyApp.Accounts
user_resource MyApp.Accounts.User user_resource MyApp.Accounts.User
end end
@ -60,7 +60,7 @@ Configure identity options for this resource
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`user_resource`](#user_identity-user_resource){: #user_identity-user_resource .spark-required} | `module` | | The user resource to which these identities belong. | | [`user_resource`](#user_identity-user_resource){: #user_identity-user_resource .spark-required} | `module` | | The user resource to which these identities belong. |
| [`api`](#user_identity-api){: #user_identity-api } | `module` | | The Ash API to use to access this resource. | | [`domain`](#user_identity-domain){: #user_identity-domain } | `module` | | The Ash domain to use to access this resource. |
| [`uid_attribute_name`](#user_identity-uid_attribute_name){: #user_identity-uid_attribute_name } | `atom` | `:uid` | The name of the `uid` attribute on this resource. | | [`uid_attribute_name`](#user_identity-uid_attribute_name){: #user_identity-uid_attribute_name } | `atom` | `:uid` | The name of the `uid` attribute on this resource. |
| [`strategy_attribute_name`](#user_identity-strategy_attribute_name){: #user_identity-strategy_attribute_name } | `atom` | `:strategy` | The name of the `strategy` attribute on this resource. | | [`strategy_attribute_name`](#user_identity-strategy_attribute_name){: #user_identity-strategy_attribute_name } | `atom` | `:strategy` | The name of the `strategy` attribute on this resource. |
| [`user_id_attribute_name`](#user_identity-user_id_attribute_name){: #user_identity-user_id_attribute_name } | `atom` | `:user_id` | The name of the `user_id` attribute on this resource. | | [`user_id_attribute_name`](#user_identity-user_id_attribute_name){: #user_identity-user_id_attribute_name } | `atom` | `:user_id` | The name of the `user_id` attribute on this resource. |

View file

@ -17,7 +17,8 @@ the `AshAuthentication` extension on your resource:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -26,8 +27,6 @@ defmodule MyApp.Accounts.User do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
identity_field :email identity_field :email
@ -101,7 +100,7 @@ Configure authentication for this resource
| Name | Type | Default | Docs | | Name | Type | Default | Docs |
|------|------|---------|------| |------|------|---------|------|
| [`subject_name`](#authentication-subject_name){: #authentication-subject_name } | `atom` | | The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`). | | [`subject_name`](#authentication-subject_name){: #authentication-subject_name } | `atom` | | The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`). |
| [`api`](#authentication-api){: #authentication-api } | `module` | | The name of the Ash API to use to access this resource when doing anything authenticaiton related. | | [`domain`](#authentication-domain){: #authentication-domain } | `module` | | The name of the Ash domain to use to access this resource when doing anything authentication related. |
| [`get_by_subject_action_name`](#authentication-get_by_subject_action_name){: #authentication-get_by_subject_action_name } | `atom` | `:get_by_subject` | The name of the read action used to retrieve records. If the action doesn't exist, one will be generated for you. | | [`get_by_subject_action_name`](#authentication-get_by_subject_action_name){: #authentication-get_by_subject_action_name } | `atom` | `:get_by_subject` | The name of the read action used to retrieve records. If the action doesn't exist, one will be generated for you. |
| [`select_for_senders`](#authentication-select_for_senders){: #authentication-select_for_senders } | `list(atom)` | | A list of fields that we will ensure are selected whenever a sender will be invoked. Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]` otherwise. | | [`select_for_senders`](#authentication-select_for_senders){: #authentication-select_for_senders } | `list(atom)` | | A list of fields that we will ensure are selected whenever a sender will be invoked. Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]` otherwise. |

View file

@ -4,30 +4,30 @@ AshAuthentication allows you to bring your own authentication strategy without
having to change the Ash Authentication codebase. having to change the Ash Authentication codebase.
> There is functionally no difference between "add ons" and "strategies" other > There is functionally no difference between "add ons" and "strategies" other
> than where they appear in the DSL. We invented "add ons" because it felt > than where they appear in the DSL. We invented "add ons" because it felt
> weird calling "confirmation" an authentication strategy. > weird calling "confirmation" an authentication strategy.
There are several moving parts which must all work together so hold on to your hat! There are several moving parts which must all work together so hold on to your hat!
1. A `Spark.Dsl.Entity` struct. This is used to define the strategy DSL 1. A `Spark.Dsl.Entity` struct. This is used to define the strategy DSL
inside the `strategies` (or `add_ons`) section of the `authentication` DSL. inside the `strategies` (or `add_ons`) section of the `authentication` DSL.
2. A strategy struct, which stores information about the strategy as 2. A strategy struct, which stores information about the strategy as
configured on a resource which must comply with a few rules. configured on a resource which must comply with a few rules.
3. An optional transformer, which can be used to manipulate the DSL state of 3. An optional transformer, which can be used to manipulate the DSL state of
the entity and the resource. the entity and the resource.
4. An optional verifier, which can be used to verify the DSL state of the 4. An optional verifier, which can be used to verify the DSL state of the
entity and the resource after compilation. entity and the resource after compilation.
4. The `AshAuthentication.Strategy` protocol, which provides the glue needed 5. The `AshAuthentication.Strategy` protocol, which provides the glue needed
for everything to wire up and wrappers around the actions needed to run on for everything to wire up and wrappers around the actions needed to run on
the resource. the resource.
We're going to define an extremely dumb strategy which lets anyone with a name We're going to define an extremely dumb strategy which lets anyone with a name
that starts with "Marty" sign in with just their name. Of course you would that starts with "Marty" sign in with just their name. Of course you would
never do this in real life, but this isn't real life - it's documentation! never do this in real life, but this isn't real life - it's documentation!
## DSL setup ## DSL setup
Let's start by defining a module for our strategy to live in. Let's call it Let's start by defining a module for our strategy to live in. Let's call it
`OnlyMartiesAtTheParty`: `OnlyMartiesAtTheParty`:
```elixir ```elixir
@ -36,7 +36,7 @@ defmodule OnlyMartiesAtTheParty do
end end
``` ```
Sadly, this isn't enough to make the magic happen. We need to define our DSL Sadly, this isn't enough to make the magic happen. We need to define our DSL
entity by adding it to the `use` statement: entity by adding it to the `use` statement:
```elixir ```elixir
@ -87,22 +87,22 @@ end
If you haven't you should take a look at the docs for `Spark.Dsl.Entity`, but If you haven't you should take a look at the docs for `Spark.Dsl.Entity`, but
here's a brief overview of what each field we've set does: here's a brief overview of what each field we've set does:
- `name` is the name for which the helper function will be generated in - `name` is the name for which the helper function will be generated in
the DSL (ie `only_marty do #... end`). the DSL (ie `only_marty do #... end`).
- `describe` and `examples` are used when generating documentation. - `describe` and `examples` are used when generating documentation.
- `target` is the name of the module which defines our entity struct. We've - `target` is the name of the module which defines our entity struct. We've
set it to `__MODULE__` which means that we'll have to define the struct on set it to `__MODULE__` which means that we'll have to define the struct on
this module. this module.
- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark - `schema` is a keyword list that defines a `NimbleOptions` schema. Spark
provides a number of additional types over the default ones though, so check provides a number of additional types over the default ones though, so check
out `Spark.OptionsHelpers` for more information. out `Spark.OptionsHelpers` for more information.
> By default the entity is added to the `authentication / strategy` DSL, however > By default the entity is added to the `authentication / strategy` DSL, however
> if you want it in the `authentication / add_ons` DSL instead you can also pass > if you want it in the `authentication / add_ons` DSL instead you can also pass
> `style: :add_on` in the `use` statement. > `style: :add_on` in the `use` statement.
Next up, we need to define our struct. The struct should have *at least* the Next up, we need to define our struct. The struct should have _at least_ the
fields named in the entity schema. Additionally, Ash Authentication requires fields named in the entity schema. Additionally, Ash Authentication requires
that it have a `resource` field which will be set to the module of the resource that it have a `resource` field which will be set to the module of the resource
it's attached to during compilation. it's attached to during compilation.
@ -123,11 +123,11 @@ by adding it to the `extensions` section of your resource:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication, OnlyMartiesAtTheParty] use Ash.Resource,
extensions: [AshAuthentication, OnlyMartiesAtTheParty],
domain: MyApp.Accounts
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
only_marty do only_marty do
name_field :name name_field :name
@ -145,25 +145,25 @@ end
## Implementing the `AshAuthentication.Strategy` protocol ## Implementing the `AshAuthentication.Strategy` protocol
The Strategy protocol is used to introspect the strategy so that it can The Strategy protocol is used to introspect the strategy so that it can
seamlessly fit in with the rest of Ash Authentication. Here are the key seamlessly fit in with the rest of Ash Authentication. Here are the key
concepts: concepts:
- "phases" - in terms of HTTP, each strategy is likely to have many phases (eg - "phases" - in terms of HTTP, each strategy is likely to have many phases (eg
OAuth 2.0's "request" and "callback" phases). Essentially you need one OAuth 2.0's "request" and "callback" phases). Essentially you need one
phase for each HTTP endpoint you wish to support with your strategy. In our phase for each HTTP endpoint you wish to support with your strategy. In our
case we just want one sign in endpoint. case we just want one sign in endpoint.
- "actions" - actions are exactly as they sound - Resource actions which can - "actions" - actions are exactly as they sound - Resource actions which can
be executed by the strategy, whether generated by the strategy (as in the be executed by the strategy, whether generated by the strategy (as in the
password strategy) or typed in by the user (as in the OAuth 2.0 strategy). password strategy) or typed in by the user (as in the OAuth 2.0 strategy).
The reason that we wrap the strategy's actions this way is that all the The reason that we wrap the strategy's actions this way is that all the
built-in strategies (and we hope yours too) allow the user to customise the built-in strategies (and we hope yours too) allow the user to customise the
name of the actions that it uses. At the very least it should probably name of the actions that it uses. At the very least it should probably
append the strategy name to the action. Using `Strategy.action/4` allows us append the strategy name to the action. Using `Strategy.action/4` allows us
to refer these by a more generic name rather than via the user-specified one to refer these by a more generic name rather than via the user-specified one
(eg `:register` vs `:register_with_password`). (eg `:register` vs `:register_with_password`).
- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`) - "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`)
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
`routes/1` callback is used to retrieve this information from the strategy. `routes/1` callback is used to retrieve this information from the strategy.
Given this information, let's implement the strategy. It's quite long, so I'm Given this information, let's implement the strategy. It's quite long, so I'm
going to break it up into smaller chunks. going to break it up into smaller chunks.
@ -172,8 +172,8 @@ going to break it up into smaller chunks.
defimpl AshAuthentication.Strategy, for: OnlyMartiesAtTheParty do defimpl AshAuthentication.Strategy, for: OnlyMartiesAtTheParty do
``` ```
The `name/1` function is used to uniquely identify the strategy. It *must* be an The `name/1` function is used to uniquely identify the strategy. It _must_ be an
atom and *should* be the same as the path fragment used in the generated routes. atom and _should_ be the same as the path fragment used in the generated routes.
```elixir ```elixir
def name(strategy), do: strategy.name def name(strategy), do: strategy.name
@ -187,7 +187,7 @@ and action.
def actions(_), do: [:sign_in] def actions(_), do: [:sign_in]
``` ```
Next we generate the routes for the strategy. Routes *should* contain the Next we generate the routes for the strategy. Routes _should_ contain the
subject name of the resource being authenticated in case the implementer is subject name of the resource being authenticated in case the implementer is
authenticating multiple different resources - eg `User` and `Admin`. authenticating multiple different resources - eg `User` and `Admin`.
@ -207,8 +207,8 @@ When generating routes or forms for this phase, what HTTP method should we use?
def method_for_phase(_, :sign_in), do: :post def method_for_phase(_, :sign_in), do: :post
``` ```
Next up, we write our plug. We take the "name field" from the input params in Next up, we write our plug. We take the "name field" from the input params in
the conn and pass them to our sign in action. As long as the action returns the conn and pass them to our sign in action. As long as the action returns
`{:ok, Ash.Resource.record}` or `{:error, any}` then we can just pass it `{:ok, Ash.Resource.record}` or `{:error, any}` then we can just pass it
straight into `store_authentication_result/2` from straight into `store_authentication_result/2` from
`AshAuthentication.Plug.Helpers`. `AshAuthentication.Plug.Helpers`.
@ -223,11 +223,11 @@ straight into `store_authentication_result/2` from
end end
``` ```
Finally, we implement our sign in action. We use `Ash.Query` to find all Finally, we implement our sign in action. We use `Ash.Query` to find all
records whose name field matches the input, then constrain it to only records records whose name field matches the input, then constrain it to only records
whose name field starts with "Marty". Depending on whether the name field has a whose name field starts with "Marty". Depending on whether the name field has a
unique identity on it we have to deal with it returning zero or more users, or unique identity on it we have to deal with it returning zero or more users, or
an error. When it returns a single user we return that user in an ok tuple, an error. When it returns a single user we return that user in an ok tuple,
otherwise we return an authentication failure. otherwise we return an authentication failure.
In this example we're assuming that there is a default `read` action present on In this example we're assuming that there is a default `read` action present on
@ -246,22 +246,23 @@ the resource.
```elixir ```elixir
alias AshAuthentication.Errors.AuthenticationFailed alias AshAuthentication.Errors.AuthenticationFailed
require Ash.Query require Ash.Query
import Ash.Expr
def action(strategy, :sign_in, params, options) do def action(strategy, :sign_in, params, options) do
name_field = strategy.name_field name_field = strategy.name_field
name = Map.get(params, to_string(name_field)) name = Map.get(params, to_string(name_field))
api = AshAuthentication.Info.authentication_api!(strategy.resource) domain = AshAuthentication.Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Ash.Query.filter(ref(^name_field) == ^name) |> Ash.Query.filter(expr(^ref(name_field) == ^name))
|> then(fn query -> |> then(fn query ->
if strategy.case_sensitive? do if strategy.case_sensitive? do
Ash.Query.filter(query, like(ref(^name_field), "Marty%")) Ash.Query.filter(query, like(^ref(name_field), "Marty%"))
else else
Ash.Query.filter(query, ilike(ref(^name_field), "Marty%")) Ash.Query.filter(query, ilike(^ref(name_field), "Marty%"))
end end
end) end)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}
@ -282,7 +283,7 @@ end
## Bonus round - transformers and verifiers ## Bonus round - transformers and verifiers
In some cases it may be required for your strategy to modify it's own In some cases it may be required for your strategy to modify it's own
configuration or that of the whole resource at compile time. For that you can configuration or that of the whole resource at compile time. For that you can
define the `transform/2` callback on your strategy module. define the `transform/2` callback on your strategy module.
At the very least it is good practice to call At the very least it is good practice to call
@ -294,7 +295,7 @@ imported by `use AshAuthentication.Strategy.Custom` for this purpose.
### Transformers ### Transformers
For simple cases where you're just transforming the strategy you can just return For simple cases where you're just transforming the strategy you can just return
the modified strategy and the DSL will be updated accordingly. For example if the modified strategy and the DSL will be updated accordingly. For example if
you wanted to generate the name of an action if the user hasn't specified it: you wanted to generate the name of an action if the user hasn't specified it:
```elixir ```elixir
@ -303,9 +304,9 @@ def transform(strategy, _dsl_state) do
end end
``` ```
In some cases you may want to modify the strategy and the resources DSL. In In some cases you may want to modify the strategy and the resources DSL. In
this case you can return the newly mutated DSL state in an ok tuple or an error this case you can return the newly mutated DSL state in an ok tuple or an error
tuple, preferably containing a `Spark.Error.DslError`. For example if we wanted tuple, preferably containing a `Spark.Error.DslError`. For example if we wanted
to build a sign in action for `OnlyMartiesAtTheParty` to use: to build a sign in action for `OnlyMartiesAtTheParty` to use:
```elixir ```elixir
@ -332,12 +333,12 @@ end
``` ```
Transformers can also be used to validate user input or even directly add code Transformers can also be used to validate user input or even directly add code
to the resource. See the docs for `Spark.Dsl.Transformer` for more information. to the resource. See the docs for `Spark.Dsl.Transformer` for more information.
### Verifiers ### Verifiers
We also support a variant of transformers which run in the new `@after_verify` We also support a variant of transformers which run in the new `@after_verify`
compile hook provided by Elixir 1.14. This is a great place to put checks compile hook provided by Elixir 1.14. This is a great place to put checks
to make sure that the user's configuration makes sense without adding any to make sure that the user's configuration makes sense without adding any
compile-time dependencies between modules which may cause compiler deadlocks. compile-time dependencies between modules which may cause compiler deadlocks.

View file

@ -6,41 +6,45 @@ Auth0 for authentication.
Before you start this tutorial, skip the Token resource while following the Before you start this tutorial, skip the Token resource while following the
[AshAuthenticationPhoenix guide](https://hexdocs.pm/ash_authentication_phoenix/getting-started-with-ash-authentication-phoenix.html)) [AshAuthenticationPhoenix guide](https://hexdocs.pm/ash_authentication_phoenix/getting-started-with-ash-authentication-phoenix.html))
> [!WARNING] > [!WARNING]
> Make sure that your `ash_postgres` dependency is `~> 1.3.64`. A bug in previous versions prevents the action shown below from working correctly. > Make sure that your `ash_postgres` dependency is `~> 1.3.64`. A bug in previous versions prevents the action shown below from working correctly.
Next, you need to configure an application in [the Auth0 Next, you need to configure an application in [the Auth0
dashboard](https://manage.auth0.com/) using the following steps: dashboard](https://manage.auth0.com/) using the following steps:
1. Click "Create Application". 1. Click "Create Application".
2. Set your application name to something that identifies it. You will likely 2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so need separate applications for development and production environments, so
keep that in mind. keep that in mind.
3. Select "Regular Web Application" and click "Create". 3. Select "Regular Web Application" and click "Create".
4. Switch to the "Settings" tab. 4. Switch to the "Settings" tab.
5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll 5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll
need them soon. need them soon.
6. In the "Allowed Callback URLs" section, add your callback URL. The 6. In the "Allowed Callback URLs" section, add your callback URL. The
callback URL is generated from the following information: callback URL is generated from the following information:
- The base URL of the application - in development that would be
`http://localhost:4000/` but in production will be your application's
URL.
- The mount point of the auth routes in your router - we'll assume
`/auth`.
- The "subject name" of the resource being authenticated - we'll assume `user`.
- The name of the strategy in your configuration. By default this is
`auth0`.
This means that the callback URL should look something like - The base URL of the application - in development that would be
`http://localhost:4000/auth/user/auth0/callback`. `http://localhost:4000/` but in production will be your application's
7. Set "Allowed Web Origins" to your application's base URL. URL.
8. Click "Save Changes". - The mount point of the auth routes in your router - we'll assume
`/auth`.
- The "subject name" of the resource being authenticated - we'll assume `user`.
- The name of the strategy in your configuration. By default this is
`auth0`.
This means that the callback URL should look something like
`http://localhost:4000/auth/user/auth0/callback`.
7. Set "Allowed Web Origins" to your application's base URL.
8. Click "Save Changes".
Next we can configure our resource: Next we can configure our resource:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
strategies do strategies do
@ -92,29 +96,32 @@ end
The values for this configuration should be: The values for this configuration should be:
* `client_id` - the client ID copied from the Auth0 settings page. - `client_id` - the client ID copied from the Auth0 settings page.
* `redirect_uri` - the URL to the generated auth routes in your application - `redirect_uri` - the URL to the generated auth routes in your application
(eg `http://localhost:4000/auth`). (eg `http://localhost:4000/auth`).
* `client_secret` the client secret copied from the Auth0 settings page. - `client_secret` the client secret copied from the Auth0 settings page.
* `base_url` - the "domain" value copied from the Auth0 settings page prefixed - `base_url` - the "domain" value copied from the Auth0 settings page prefixed
with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`). with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`).
Lastly, we need to add a register action to your user resource. This is defined Lastly, we need to add a register action to your user resource. This is defined
as an upsert so that it can register new users, or update information for as an upsert so that it can register new users, or update information for
returning users. The default name of the action is `register_with_` followed by returning users. The default name of the action is `register_with_` followed by
the strategy name. In our case that is `register_with_auth0`. the strategy name. In our case that is `register_with_auth0`.
The register action takes two arguments, `user_info` and the `oauth_tokens`. The register action takes two arguments, `user_info` and the `oauth_tokens`.
- `user_info` contains the [`GET /userinfo` response from
Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you - `user_info` contains the [`GET /userinfo` response from
can use to populate your user attributes as needed. Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you
- `oauth_tokens` contains the [`POST /oauth/token` response from can use to populate your user attributes as needed.
Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want - `oauth_tokens` contains the [`POST /oauth/token` response from
to store these if you intend to call the Auth0 API on behalf of the user. Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want
to store these if you intend to call the Auth0 API on behalf of the user.
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
# ... # ...

View file

@ -49,8 +49,8 @@ used when converting tokens or session information into a resource record.
### `AshAuthentication.Strategy.Password` ### `AshAuthentication.Strategy.Password`
This authentication strategy provides registration and sign-in for users using a local This authentication strategy provides registration and sign-in for users using a local
identifier (eg `username`, `email` or `phone_number`) and a password. It will identifier (eg `username`, `email` or `phone_number`) and a password. It will
define register and sign-in actions on your "user" resource. You are welcome to define register and sign-in actions on your "user" resource. You are welcome to
define either or both of these actions yourself if you wish to customise them - define either or both of these actions yourself if you wish to customise them -
if you do so then the extension will do it's best to validate that all required if you do so then the extension will do it's best to validate that all required
configuration is present. configuration is present.
@ -60,7 +60,7 @@ The `AshAuthentication.Strategy.Password` DSL allows you to override any of the
### `AshAuthentication.Strategy.OAuth2` ### `AshAuthentication.Strategy.OAuth2`
This authentication strategy provides registration and sign-in for users using a This authentication strategy provides registration and sign-in for users using a
remote [OAuth 2.0](https://oauth.net/2/) server as the source of truth. You remote [OAuth 2.0](https://oauth.net/2/) server as the source of truth. You
will be required to provide either a "register" or a "sign-in" action depending will be required to provide either a "register" or a "sign-in" action depending
on your configuration, which the strategy will attempt to validate for common on your configuration, which the strategy will attempt to validate for common
misconfigurations. misconfigurations.
@ -74,7 +74,7 @@ change to take place.
### `AshAuthentication.TokenResource` ### `AshAuthentication.TokenResource`
This extension allows you to easily create a resource which will store This extension allows you to easily create a resource which will store
information about tokens that can't be encoded into the tokens themselves. A information about tokens that can't be encoded into the tokens themselves. A
resource with this extension must be present if token generation is enabled. resource with this extension must be present if token generation is enabled.
### `AshAuthentication.UserIdentity` ### `AshAuthentication.UserIdentity`
@ -82,21 +82,21 @@ resource with this extension must be present if token generation is enabled.
If you plan to support multiple different strategies at once (eg giving your If you plan to support multiple different strategies at once (eg giving your
users the choice of more than one authentication provider, or signing them into users the choice of more than one authentication provider, or signing them into
multiple services simultaneously) then you will want to create a resource with multiple services simultaneously) then you will want to create a resource with
this extension enabled. It is used to keep track of the links between your this extension enabled. It is used to keep track of the links between your
local user records and their many remote identities. local user records and their many remote identities.
## Example ## Example
Let's create an `Accounts` API in our application which provides a `User` Let's create an `Accounts` domain in our application which provides a `User`
resource and a `Token` resource. resource and a `Token` resource.
First, let's define our API: First, let's define our domain:
```elixir ```elixir
# lib/my_app/accounts.ex # lib/my_app/accounts.ex
defmodule MyApp.Accounts do defmodule MyApp.Accounts do
use Ash.Api use Ash.Domain
resources do resources do
resource MyApp.Accounts.User resource MyApp.Accounts.User
@ -105,15 +105,15 @@ defmodule MyApp.Accounts do
end end
``` ```
Be sure to add it to the `ash_apis` config in your `config.exs` Be sure to add it to the `ash_domains` config in your `config.exs`
```elixir ```elixir
# in config/config.exs # in config/config.exs
config :my_app, :ash_apis: [..., MyApp.Accounts] config :my_app, :ash_domains: [..., MyApp.Accounts]
``` ```
Next, let's define our `Token` resource. This resource is needed Next, let's define our `Token` resource. This resource is needed
if token generation is enabled for any resources in your application. Most of if token generation is enabled for any resources in your application. Most of
the contents are auto-generated, so we just need to provide the data layer the contents are auto-generated, so we just need to provide the data layer
configuration and the API to use. configuration and the API to use.
@ -124,7 +124,7 @@ But before we do, we need to install a postgres extension.
defmodule MyApp.Repo do defmodule MyApp.Repo do
use AshPostgres.Repo, otp_app: :my_app use AshPostgres.Repo, otp_app: :my_app
def installed_extensions do def installed_extensions do
["uuid-ossp", "citext"] ["uuid-ossp", "citext"]
end end
@ -140,11 +140,8 @@ You can skip this step if you don't want to use tokens, in which case remove the
defmodule MyApp.Accounts.Token do defmodule MyApp.Accounts.Token do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource] extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
token do
api MyApp.Accounts
end
postgres do postgres do
table "tokens" table "tokens"
@ -170,17 +167,16 @@ defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication], extensions: [AshAuthentication],
authorizers: [Ash.Policy.Authorizer] authorizers: [Ash.Policy.Authorizer],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false attribute :email, :ci_string, allow_nil?: false, public?: true
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true, private?: true attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
identity_field :email identity_field :email
@ -231,8 +227,8 @@ If you're using Phoenix, then you can skip this section and go straight to
[Integrating Ash Authentication and Phoenix](https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix) [Integrating Ash Authentication and Phoenix](https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix)
In order for your users to be able to sign in, you will likely need to provide In order for your users to be able to sign in, you will likely need to provide
an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication
provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro
which handles routing of requests to the correct providers, and defines which handles routing of requests to the correct providers, and defines
callbacks for successful and unsuccessful outcomes. callbacks for successful and unsuccessful outcomes.
@ -290,7 +286,7 @@ based on the contents of the session store or `Authorization` header.
## Supervisor ## Supervisor
AshAuthentication includes a supervisor which you should add to your AshAuthentication includes a supervisor which you should add to your
application's supervisor tree. This is used to run any periodic jobs related to application's supervisor tree. This is used to run any periodic jobs related to
your authenticated resources (removing expired tokens, for example). your authenticated resources (removing expired tokens, for example).
### Example ### Example
@ -314,9 +310,9 @@ end
## Token generation ## Token generation
If you have token generation enabled then you need to provide (at minimum) a If you have token generation enabled then you need to provide (at minimum) a
signing secret. As the name implies this should be a secret. AshAuthentication signing secret. As the name implies this should be a secret. AshAuthentication
provides a mechanism for looking up secrets at runtime using the provides a mechanism for looking up secrets at runtime using the
`AshAuthentication.Secret` behaviour. To save you a click, this means that you `AshAuthentication.Secret` behaviour. To save you a click, this means that you
can set your token signing secret using either a static string (please don't!), can set your token signing secret using either a static string (please don't!),
a two-arity anonymous function, or a module which implements the a two-arity anonymous function, or a module which implements the
`AshAuthentication.Secret` behaviour. `AshAuthentication.Secret` behaviour.

View file

@ -6,37 +6,41 @@ GitHub for authentication.
First you need to configure an application in your [GitHub developer First you need to configure an application in your [GitHub developer
settings](https://github.com/settings/developers): settings](https://github.com/settings/developers):
1. Click the "New OAuth App" button. 1. Click the "New OAuth App" button.
2. Set your application name to something that identifies it. You will likely 2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so need separate applications for development and production environments, so
keep that in mind. keep that in mind.
3. Set "Homepage URL" appropriately for your application and environment. 3. Set "Homepage URL" appropriately for your application and environment.
4. In the "Authorization callback URL" section, add your callback URL. The 4. In the "Authorization callback URL" section, add your callback URL. The
callback URL is generated from the following information: callback URL is generated from the following information:
- The base URL of the application - in development that would be
`http://localhost:4000/` but in production will be your application's
URL.
- The mount point of the auth routes in your router - we'll assume
`/auth`.
- The "subject name" of the resource being authenticated - we'll assume `user`.
- The name of the strategy in your configuration. By default this is
`github`.
This means that the callback URL should look something like - The base URL of the application - in development that would be
`http://localhost:4000/auth/user/github/callback`. `http://localhost:4000/` but in production will be your application's
5. Do not set "Enable Device Flow" unless you know why you want this. URL.
6. Click "Register application". - The mount point of the auth routes in your router - we'll assume
7. Click "Generate a new client secret". `/auth`.
8. Copy the "Client ID" and "Client secret" somewhere safe, we'll need them - The "subject name" of the resource being authenticated - we'll assume `user`.
soon. - The name of the strategy in your configuration. By default this is
9. Click "Update application". `github`.
This means that the callback URL should look something like
`http://localhost:4000/auth/user/github/callback`.
5. Do not set "Enable Device Flow" unless you know why you want this.
6. Click "Register application".
7. Click "Generate a new client secret".
8. Copy the "Client ID" and "Client secret" somewhere safe, we'll need them
soon.
9. Click "Update application".
Next we can configure our resource (assuming you already have everything else Next we can configure our resource (assuming you already have everything else
set up): set up):
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
strategies do strategies do
@ -82,28 +86,31 @@ end
The values for this configuration should be: The values for this configuration should be:
* `client_id` - the client ID copied from the GitHub settings page. - `client_id` - the client ID copied from the GitHub settings page.
* `redirect_uri` - the URL to the generated auth routes in your application - `redirect_uri` - the URL to the generated auth routes in your application
(eg `http://localhost:4000/auth`). (eg `http://localhost:4000/auth`).
* `client_secret` the client secret copied from the GitHub settings page. - `client_secret` the client secret copied from the GitHub settings page.
Lastly, we need to add a register action to your user resource. This is defined Lastly, we need to add a register action to your user resource. This is defined
as an upsert so that it can register new users, or update information for as an upsert so that it can register new users, or update information for
returning users. The default name of the action is `register_with_` followed by returning users. The default name of the action is `register_with_` followed by
the strategy name. In our case that is `register_with_github`. the strategy name. In our case that is `register_with_github`.
The register action takes two arguments, `user_info` and the `oauth_tokens`. The register action takes two arguments, `user_info` and the `oauth_tokens`.
- `user_info` contains the [`GET /user` response from
GitHub](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user) - `user_info` contains the [`GET /user` response from
which you can use to populate your user attributes as needed. GitHub](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user)
- `oauth_tokens` contains the [`POST /login/oauth/access_token` response from which you can use to populate your user attributes as needed.
GitHub](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response) - `oauth_tokens` contains the [`POST /login/oauth/access_token` response from
- you may want to store these if you intend to call the GitHub API on behalf GitHub](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response)
- you may want to store these if you intend to call the GitHub API on behalf
of the user. of the user.
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
# ... # ...

View file

@ -10,9 +10,11 @@ First you'll need a registered application in [Google Cloud](https://console.clo
Next we configure our resource to use google credentials: Next we configure our resource to use google credentials:
``` elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
... ...
@ -34,9 +36,12 @@ end
Please check the guide on how to properly configure your Secrets Please check the guide on how to properly configure your Secrets
Then we need to define an action that will handle the oauth2 flow, for the google case it is `:register_with_google` it will handle both cases for our resource, user registration & login. Then we need to define an action that will handle the oauth2 flow, for the google case it is `:register_with_google` it will handle both cases for our resource, user registration & login.
``` elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
# ... # ...
actions do actions do
create :register_with_google do create :register_with_google do

View file

@ -16,7 +16,8 @@ defmodule AshAuthentication do
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -25,8 +26,6 @@ defmodule AshAuthentication do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
identity_field :email identity_field :email
@ -83,7 +82,7 @@ defmodule AshAuthentication do
for more information. for more information.
""" """
alias Ash.{ alias Ash.{
Api, Domain,
Error.Query.NotFound, Error.Query.NotFound,
Query, Query,
Resource Resource
@ -120,7 +119,7 @@ defmodule AshAuthentication do
require Ash.Query require Ash.Query
@type resource_config :: %{ @type resource_config :: %{
api: module, domain: module,
providers: [module], providers: [module],
resource: module, resource: module,
subject_name: atom subject_name: atom
@ -145,8 +144,8 @@ defmodule AshAuthentication do
|> List.wrap() |> List.wrap()
|> Enum.flat_map(fn otp_app -> |> Enum.flat_map(fn otp_app ->
otp_app otp_app
|> Application.get_env(:ash_apis, []) |> Application.get_env(:ash_domains, [])
|> Stream.flat_map(&Api.Info.resources(&1)) |> Stream.flat_map(&Domain.Info.resources(&1))
|> Stream.uniq() |> Stream.uniq()
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1))) |> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|> Enum.to_list() |> Enum.to_list()
@ -185,7 +184,7 @@ defmodule AshAuthentication do
iex> %{id: user_id} = build_user() iex> %{id: user_id} = build_user()
...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User) ...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User)
Any options passed will be passed to the underlying `Api.read/2` callback. Any options passed will be passed to the underlying `Domain.read/2` callback.
""" """
@spec subject_to_user(subject | URI.t(), Resource.t(), keyword) :: @spec subject_to_user(subject | URI.t(), Resource.t(), keyword) ::
{:ok, Resource.record()} | {:error, any} {:ok, Resource.record()} | {:error, any}
@ -199,7 +198,7 @@ defmodule AshAuthentication do
with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource), with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource),
^subject_name <- to_string(resource_subject_name), ^subject_name <- to_string(resource_subject_name),
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource), {:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource),
{:ok, api} <- Info.authentication_api(resource) do {:ok, domain} <- Info.domain(resource) do
primary_key = primary_key =
primary_key primary_key
|> URI.decode_query() |> URI.decode_query()
@ -214,7 +213,7 @@ defmodule AshAuthentication do
}) })
|> Query.for_read(action_name, %{}) |> Query.for_read(action_name, %{})
|> Query.filter(^primary_key) |> Query.filter(^primary_key)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, user} {:ok, [user]} -> {:ok, user}
_ -> {:error, NotFound.exception([])} _ -> {:error, NotFound.exception([])}

View file

@ -21,7 +21,8 @@ defmodule AshAuthentication.AddOn.Confirmation do
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -29,8 +30,6 @@ defmodule AshAuthentication.AddOn.Confirmation do
end end
authentication do authentication do
api MyApp.Accounts
add_ons do add_ons do
confirmation :confirm do confirmation :confirm do
monitor_fields [:email] monitor_fields [:email]

View file

@ -21,7 +21,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
""" """
@spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any} @spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def confirm(strategy, params, opts \\ []) do def confirm(strategy, params, opts \\ []) do
with {:ok, api} <- Info.authentication_api(strategy.resource), with {:ok, domain} <- Info.domain(strategy.resource),
{:ok, token} <- Map.fetch(params, "confirm"), {:ok, token} <- Map.fetch(params, "confirm"),
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource), {:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
{:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts) do {:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts) do
@ -33,7 +33,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
} }
}) })
|> Changeset.for_update(strategy.confirm_action_name, params) |> Changeset.for_update(strategy.confirm_action_name, params)
|> api.update(opts) |> domain.update(opts)
else else
:error -> {:error, InvalidToken.exception(type: :confirmation)} :error -> {:error, InvalidToken.exception(type: :confirmation)}
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -52,7 +52,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|> Map.new() |> Map.new()
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource), with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
{:ok, api} <- TokenResource.Info.token_api(token_resource), {:ok, domain} <- TokenResource.Info.token_domain(token_resource),
{:ok, store_changes_action} <- {:ok, store_changes_action} <-
TokenResource.Info.token_confirmation_store_changes_action_name(token_resource), TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
{:ok, _token_record} <- {:ok, _token_record} <-
@ -68,7 +68,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
extra_data: changes, extra_data: changes,
purpose: to_string(Strategy.name(strategy)) purpose: to_string(Strategy.name(strategy))
}) })
|> api.create(Keyword.merge(opts, upsert?: true)) do |> domain.create(Keyword.merge(opts, upsert?: true)) do
:ok :ok
else else
{:error, reason} -> {:error, reason} ->
@ -88,7 +88,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
@spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error @spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
def get_changes(strategy, jti, opts \\ []) do def get_changes(strategy, jti, opts \\ []) do
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource), with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
{:ok, api} <- TokenResource.Info.token_api(token_resource), {:ok, domain} <- TokenResource.Info.token_domain(token_resource),
{:ok, get_changes_action} <- {:ok, get_changes_action} <-
TokenResource.Info.token_confirmation_get_changes_action_name(token_resource), TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
{:ok, [token_record]} <- {:ok, [token_record]} <-
@ -101,7 +101,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
}) })
|> Query.set_context(%{strategy: strategy}) |> Query.set_context(%{strategy: strategy})
|> Query.for_read(get_changes_action, %{"jti" => jti}) |> Query.for_read(get_changes_action, %{"jti" => jti})
|> api.read(opts) do |> domain.read(opts) do
changes = changes =
strategy.monitor_fields strategy.monitor_fields
|> Stream.map(&to_string/1) |> Stream.map(&to_string/1)

View file

@ -29,6 +29,11 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
defp do_change(changeset, strategy) do defp do_change(changeset, strategy) do
changeset changeset
|> Changeset.set_context(%{
private: %{
ash_authentication?: true
}
})
|> Changeset.before_action(fn changeset -> |> Changeset.before_action(fn changeset ->
with token when is_binary(token) <- Changeset.get_argument(changeset, :confirm), with token when is_binary(token) <- Changeset.get_argument(changeset, :confirm),
{:ok, %{"act" => action, "jti" => jti}, _} <- {:ok, %{"act" => action, "jti" => jti}, _} <-
@ -41,11 +46,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
else: %{} else: %{}
changeset changeset
|> Changeset.change_attributes(allowed_changes) |> Changeset.force_change_attributes(allowed_changes)
|> Changeset.change_attribute(strategy.confirmed_at_field, DateTime.utc_now()) |> Changeset.change_attribute(strategy.confirmed_at_field, DateTime.utc_now())
else else
_ -> _ ->
raise InvalidArgument, field: :confirm, message: "is not valid" changeset
|> Changeset.add_error(
InvalidArgument.exception(field: :confirm, message: "is not valid")
)
end end
end) end)
end end

View file

@ -53,15 +53,18 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
defp do_change(changeset, strategy) do defp do_change(changeset, strategy) do
changeset changeset
|> Changeset.before_action(fn changeset -> |> Changeset.before_action(
changeset fn changeset ->
|> not_confirm_action(strategy) changeset
|> should_confirm_action_type(strategy) |> not_confirm_action(strategy)
|> monitored_field_changing(strategy) |> should_confirm_action_type(strategy)
|> changes_would_be_valid() |> monitored_field_changing(strategy)
|> maybe_inhibit_updates(strategy) |> changes_would_be_valid()
|> maybe_perform_confirmation(strategy, changeset) |> maybe_inhibit_updates(strategy)
end) |> maybe_perform_confirmation(strategy, changeset)
end,
prepend?: true
)
end end
defp not_confirm_action(%Changeset{} = changeset, strategy) defp not_confirm_action(%Changeset{} = changeset, strategy)

View file

@ -83,6 +83,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
with {:ok, resource} <- persisted_option(dsl_state, :module), with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, field), {:ok, attribute} <- find_attribute(dsl_state, field),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]), :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :public?, [true]),
:ok <- maybe_validate_eager_checking(dsl_state, strategy, field, resource) do :ok <- maybe_validate_eager_checking(dsl_state, strategy, field, resource) do
{:cont, :ok} {:cont, :ok}
else else
@ -142,7 +143,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
accept: strategy.monitor_fields, accept: strategy.monitor_fields,
arguments: arguments, arguments: arguments,
metadata: metadata, metadata: metadata,
changes: changes changes: changes,
require_atomic?: false
) )
end end
@ -150,8 +152,26 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
with {:ok, action} <- validate_action_exists(dsl_state, strategy.confirm_action_name), with {:ok, action} <- validate_action_exists(dsl_state, strategy.confirm_action_name),
:ok <- validate_action_has_change(action, Confirmation.ConfirmChange), :ok <- validate_action_has_change(action, Confirmation.ConfirmChange),
:ok <- validate_action_argument_option(action, :confirm, :allow_nil?, [false]), :ok <- validate_action_argument_option(action, :confirm, :allow_nil?, [false]),
:ok <- validate_action_has_change(action, GenerateTokenChange) do :ok <- validate_action_argument_option(action, :confirm, :type, [Type.String]),
validate_action_argument_option(action, :confirm, :type, [Type.String]) :ok <- validate_action_has_change(action, GenerateTokenChange),
:ok <- validate_action_option(action, :require_atomic?, [false]) do
accept_fields = MapSet.new(action.accept)
strategy.monitor_fields
|> MapSet.new()
|> MapSet.difference(accept_fields)
|> Enum.to_list()
|> case do
[] ->
:ok
_fields ->
{:error,
DslError.exception(
path: [:actions, action.name, :accept],
message: "The confirmation action must accept the monitored fields."
)}
end
end end
end end

View file

@ -8,7 +8,7 @@ defmodule AshAuthentication.Dsl do
import AshAuthentication.Utils, only: [to_sentence: 2] import AshAuthentication.Utils, only: [to_sentence: 2]
import Joken.Signer, only: [algorithms: 0] import Joken.Signer, only: [algorithms: 0]
alias Ash.{Api, Resource} alias Ash.{Domain, Resource}
@default_token_lifetime_days 14 @default_token_lifetime_days 14
@ -41,17 +41,18 @@ defmodule AshAuthentication.Dsl do
%Section{ %Section{
name: :authentication, name: :authentication,
describe: "Configure authentication for this resource", describe: "Configure authentication for this resource",
modules: [:api], modules: [:domain],
schema: [ schema: [
subject_name: [ subject_name: [
type: :atom, type: :atom,
doc: doc:
"The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`)." "The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`)."
], ],
api: [ domain: [
type: {:behaviour, Api}, type: {:behaviour, Domain},
required: false,
doc: doc:
"The name of the Ash API to use to access this resource when doing anything authenticaiton related." "The name of the Ash domain to use to access this resource when doing anything authentication related."
], ],
get_by_subject_action_name: [ get_by_subject_action_name: [
type: :atom, type: :atom,

View file

@ -3,16 +3,20 @@ defmodule AshAuthentication.Errors.AuthenticationFailed do
A generic, authentication failed error. A generic, authentication failed error.
""" """
use Ash.Error.Exception use Ash.Error.Exception
def_ash_error([:field, :strategy, caused_by: %{}], class: :forbidden)
import AshAuthentication.Debug use Splode.Error,
fields: [
caused_by: %{},
changeset: nil,
field: nil,
query: nil,
strategy: nil
],
class: :forbidden
@type t :: Exception.t() @type t :: Exception.t()
def exception(args) do def message(_), do: "Authentication failed"
args
|> super()
|> describe()
end
defimpl Ash.ErrorKind do defimpl Ash.ErrorKind do
@moduledoc false @moduledoc false

View file

@ -3,7 +3,9 @@ defmodule AshAuthentication.Errors.InvalidToken do
An invalid token was presented. An invalid token was presented.
""" """
use Ash.Error.Exception use Ash.Error.Exception
def_ash_error([:type], class: :forbidden) use Splode.Error, fields: [:type], class: :forbidden
def message(%{type: type}), do: "Invalid #{type} token"
defimpl Ash.ErrorKind do defimpl Ash.ErrorKind do
@moduledoc false @moduledoc false

View file

@ -3,7 +3,11 @@ defmodule AshAuthentication.Errors.MissingSecret do
A secret is now missing. A secret is now missing.
""" """
use Ash.Error.Exception use Ash.Error.Exception
def_ash_error([:resource], class: :forbidden) use Splode.Error, fields: [:resource], class: :forbidden
def message(%{path: path, resource: resource}) do
"Secret for `#{Enum.join(path, ".")}` on the `#{inspect(resource)}` resource is not accessible."
end
defimpl Ash.ErrorKind do defimpl Ash.ErrorKind do
@moduledoc false @moduledoc false

View file

@ -7,7 +7,7 @@ defmodule AshAuthentication.Info do
extension: AshAuthentication, extension: AshAuthentication,
sections: [:authentication] sections: [:authentication]
alias Ash.{Changeset, Query} alias Ash.{Changeset, Domain, Query, Resource}
alias AshAuthentication.Strategy alias AshAuthentication.Strategy
alias Spark.Dsl.Extension alias Spark.Dsl.Extension
@ -103,4 +103,33 @@ defmodule AshAuthentication.Info do
{:ok, strategy} {:ok, strategy}
end end
end end
@doc """
Retrieve the domain to use for authentication.
If the `authentication.domain` DSL option is set, it will be used, otherwise
it will default to that configured on the resource.
"""
@spec domain(dsl_or_resource) :: {:ok, Domain.t()} | :error
def domain(dsl_or_resource) do
auth_domain =
case authentication_domain(dsl_or_resource) do
{:ok, value} -> value
:error -> nil
end
resource_domain = Resource.Info.domain(dsl_or_resource)
domain = auth_domain || resource_domain
if domain, do: {:ok, domain}, else: :error
end
@doc "Raising version of `domain/1`"
def domain!(dsl_or_resource) do
case domain(dsl_or_resource) do
{:ok, value} -> value
:error -> raise "No `domain` configured on resource `#{inspect(dsl_or_resource)}`"
end
end
end end

View file

@ -3,7 +3,7 @@ defmodule AshAuthentication.Plug.Macros do
Generators used within `use AshAuthentication.Plug`. Generators used within `use AshAuthentication.Plug`.
""" """
alias Ash.Api alias Ash.Domain
alias AshAuthentication.Plug.Helpers alias AshAuthentication.Plug.Helpers
alias Plug.Conn alias Plug.Conn
alias Spark.Dsl.Extension alias Spark.Dsl.Extension
@ -14,11 +14,9 @@ defmodule AshAuthentication.Plug.Macros do
@spec validate_subject_name_uniqueness(atom) :: Macro.t() @spec validate_subject_name_uniqueness(atom) :: Macro.t()
defmacro validate_subject_name_uniqueness(otp_app) do defmacro validate_subject_name_uniqueness(otp_app) do
quote do quote do
require Ash.Api.Info
unquote(otp_app) unquote(otp_app)
|> Application.compile_env(:ash_apis, []) |> Application.compile_env(:ash_domains, [])
|> Stream.flat_map(&Api.Info.depend_on_resources(&1)) |> Stream.flat_map(&Domain.Info.resources(&1))
|> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)}) |> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)})
|> Stream.reject(&(elem(&1, 1) == nil)) |> Stream.reject(&(elem(&1, 1) == nil))
|> Stream.map(&{elem(&1, 0), elem(&1, 1).subject_name}) |> Stream.map(&{elem(&1, 0), elem(&1, 1).subject_name})

View file

@ -22,15 +22,14 @@ defmodule AshAuthentication.Plug.Router do
|> Macro.expand_once(__CALLER__) |> Macro.expand_once(__CALLER__)
quote do quote do
require Ash.Api.Info
use Plug.Router use Plug.Router
plug(:match) plug(:match)
plug(:dispatch) plug(:dispatch)
routes = routes =
unquote(otp_app) unquote(otp_app)
|> Application.compile_env(:ash_apis, []) |> Application.compile_env(:ash_domains, [])
|> Stream.flat_map(&Ash.Api.Info.depend_on_resources(&1)) |> Stream.flat_map(&Ash.Domain.Info.resources(&1))
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1))) |> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|> Stream.flat_map(&Info.authentication_strategies/1) |> Stream.flat_map(&Info.authentication_strategies/1)
|> Stream.flat_map(fn strategy -> |> Stream.flat_map(fn strategy ->

View file

@ -15,11 +15,11 @@ defmodule AshAuthentication.Secret do
end end
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 do oauth2 do
client_id MyApp.GetSecret client_id MyApp.GetSecret
@ -34,11 +34,11 @@ defmodule AshAuthentication.Secret do
```elixir ```elixir
defmodule MyApp.User do defmodule MyApp.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 do oauth2 do
client_id fn _secret, _resource -> client_id fn _secret, _resource ->

View file

@ -41,11 +41,11 @@ defmodule AshAuthentication.Sender do
end end
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
resettable do resettable do
@ -57,15 +57,15 @@ defmodule AshAuthentication.Sender do
end end
``` ```
You can also implment it directly as a function: You can also implement it directly as a function:
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication] use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
resettable do resettable do

View file

@ -18,7 +18,8 @@ defmodule AshAuthentication.Strategy.MagicLink do
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -26,8 +27,6 @@ defmodule AshAuthentication.Strategy.MagicLink do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
magic_link do magic_link do
identity_field :email identity_field :email

View file

@ -14,13 +14,13 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
""" """
@spec request(MagicLink.t(), map, keyword) :: :ok | {:error, any} @spec request(MagicLink.t(), map, keyword) :: :ok | {:error, any}
def request(strategy, params, options) do def request(strategy, params, options) do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Query.new() |> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}}) |> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(strategy.request_action_name, params) |> Query.for_read(strategy.request_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -33,13 +33,13 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
@spec sign_in(MagicLink.t(), map, keyword) :: @spec sign_in(MagicLink.t(), map, keyword) ::
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()} {:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
def sign_in(strategy, params, options) do def sign_in(strategy, params, options) do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Query.new() |> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}}) |> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(strategy.sign_in_action_name, params) |> Query.for_read(strategy.sign_in_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}

View file

@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _opts, _context) do def prepare(query, _opts, _context) do
strategy = Info.strategy_for_action!(query.resource, query.action.name) strategy = Info.strategy_for_action!(query.resource, query.action.name)
@ -27,7 +27,7 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
select_for_senders = Info.authentication_select_for_senders!(query.resource) select_for_senders = Info.authentication_select_for_senders!(query.resource)
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(^ref(identity_field) == ^identity)
|> Query.before_action(fn query -> |> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders) Ash.Query.ensure_selected(query, select_for_senders)
end) end)

View file

@ -10,7 +10,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _otps, _context) do def prepare(query, _otps, _context) do
subject_name = subject_name =
query.resource query.resource

View file

@ -19,7 +19,8 @@ defmodule AshAuthentication.Strategy.OAuth2 do
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -27,8 +28,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
client_id "OAuth Client ID" client_id "OAuth Client ID"
@ -157,8 +156,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
registration_enabled? false registration_enabled? false
@ -195,8 +192,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
oauth2 :example do oauth2 :example do
end end

View file

@ -22,7 +22,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
)} )}
def sign_in(%OAuth2{} = strategy, params, options) do def sign_in(%OAuth2{} = strategy, params, options) do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Query.new() |> Query.new()
@ -32,7 +32,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
} }
}) })
|> Query.for_read(strategy.sign_in_action_name, params) |> Query.for_read(strategy.sign_in_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}
@ -90,7 +90,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
""" """
@spec register(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any} @spec register(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def register(%OAuth2{} = strategy, params, options) when strategy.registration_enabled? do def register(%OAuth2{} = strategy, params, options) when strategy.registration_enabled? do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create) action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create)
strategy.resource strategy.resource
@ -104,7 +104,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
upsert?: true, upsert?: true,
upsert_identity: action.upsert_identity upsert_identity: action.upsert_identity
) )
|> api.create(options) |> domain.create(options)
end end
def register(%OAuth2{} = strategy, _params, _options), def register(%OAuth2{} = strategy, _params, _options),

View file

@ -42,7 +42,7 @@ defmodule AshAuthentication.Strategy.OAuth2.IdentityChange do
"#{user_id_attribute_name}": user.id "#{user_id_attribute_name}": user.id
}) do }) do
user user
|> changeset.api.load(strategy.identity_relationship_name) |> changeset.domain.load(strategy.identity_relationship_name)
else else
:error -> :error :error -> :error
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}

View file

@ -17,7 +17,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _opts, _context) do def prepare(query, _opts, _context) do
case Info.strategy_for_action(query.resource, query.action.name) do case Info.strategy_for_action(query.resource, query.action.name) do
:error -> :error ->
@ -70,7 +70,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|> case do |> case do
{:ok, _identity} -> {:ok, _identity} ->
user user
|> query.api.load(strategy.identity_relationship_name) |> query.domain.load(strategy.identity_relationship_name)
{:error, reason} -> {:error, reason} ->
{:error, reason} {:error, reason}

View file

@ -18,7 +18,8 @@ defmodule AshAuthentication.Strategy.Password do
```elixir ```elixir
defmodule MyApp.Accounts.User do defmodule MyApp.Accounts.User do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication] extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do attributes do
uuid_primary_key :id uuid_primary_key :id
@ -27,8 +28,6 @@ defmodule AshAuthentication.Strategy.Password do
end end
authentication do authentication do
api MyApp.Accounts
strategies do strategies do
password :password do password :password do
identity_field :email identity_field :email

View file

@ -16,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()} {:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
def sign_in(strategy, params, options) def sign_in(strategy, params, options)
when is_struct(strategy, Password) and strategy.sign_in_enabled? do when is_struct(strategy, Password) and strategy.sign_in_enabled? do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
{context, options} = Keyword.pop(options, :context, []) {context, options} = Keyword.pop(options, :context, [])
@ -33,7 +33,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|> Query.new() |> Query.new()
|> Query.set_context(context) |> Query.set_context(context)
|> Query.for_read(strategy.sign_in_action_name, params) |> Query.for_read(strategy.sign_in_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}
@ -101,13 +101,13 @@ defmodule AshAuthentication.Strategy.Password.Actions do
""" """
@spec sign_in_with_token(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any} @spec sign_in_with_token(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def sign_in_with_token(strategy, params, options) when is_struct(strategy, Password) do def sign_in_with_token(strategy, params, options) when is_struct(strategy, Password) do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Query.new() |> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}}) |> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(strategy.sign_in_with_token_action_name, params) |> Query.for_read(strategy.sign_in_with_token_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}
@ -147,7 +147,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
@spec register(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any} @spec register(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
def register(strategy, params, options) def register(strategy, params, options)
when is_struct(strategy, Password) and strategy.registration_enabled? == true do when is_struct(strategy, Password) and strategy.registration_enabled? == true do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Changeset.new() |> Changeset.new()
@ -157,7 +157,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
} }
}) })
|> Changeset.for_create(strategy.register_action_name, params) |> Changeset.for_create(strategy.register_action_name, params)
|> api.create(options) |> domain.create(options)
end end
def register(strategy, _params, _options) when is_struct(strategy, Password) do def register(strategy, _params, _options) when is_struct(strategy, Password) do
@ -182,7 +182,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
params, params,
options options
) do ) do
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Query.new() |> Query.new()
@ -192,7 +192,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
} }
}) })
|> Query.for_read(resettable.request_password_reset_action_name, params) |> Query.for_read(resettable.request_password_reset_action_name, params)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -216,7 +216,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
with {:ok, token} <- Map.fetch(params, "reset_token"), with {:ok, token} <- Map.fetch(params, "reset_token"),
{:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, strategy.resource), {:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, strategy.resource),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource, options) do {:ok, user} <- AshAuthentication.subject_to_user(subject, resource, options) do
api = Info.authentication_api!(resource) domain = Info.domain!(resource)
user user
|> Changeset.new() |> Changeset.new()
@ -226,7 +226,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
} }
}) })
|> Changeset.for_update(resettable.password_reset_action_name, params) |> Changeset.for_update(resettable.password_reset_action_name, params)
|> api.update(options) |> domain.update(options)
else else
{:error, %Changeset{} = changeset} -> {:error, changeset} {:error, %Changeset{} = changeset} -> {:error, changeset}
_ -> {:error, Errors.InvalidToken.exception(type: :reset)} _ -> {:error, Errors.InvalidToken.exception(type: :reset)}

View file

@ -28,7 +28,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.{Changeset, Error.Changes.InvalidArgument, Error.Framework.AssumptionFailed}
alias Ash.{
Changeset,
Error.Changes.InvalidArgument,
Error.Framework.AssumptionFailed,
Resource.Validation
}
alias AshAuthentication.Info alias AshAuthentication.Info
@doc """ @doc """
@ -36,8 +43,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
equivalent values - if confirmation is required. equivalent values - if confirmation is required.
""" """
@impl true @impl true
@spec validate(Changeset.t(), keyword) :: :ok | {:error, String.t() | Exception.t()} @spec validate(Changeset.t(), keyword, Validation.Context.t()) ::
def validate(changeset, options) do :ok | {:error, String.t() | Exception.t()}
def validate(changeset, options, _context) do
case Info.find_strategy(changeset, options) do case Info.find_strategy(changeset, options) do
{:ok, %{confirmation_required?: true} = strategy} -> {:ok, %{confirmation_required?: true} = strategy} ->
validate_password_confirmation(changeset, strategy) validate_password_confirmation(changeset, strategy)

View file

@ -43,14 +43,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidation do
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.Changeset alias Ash.{Changeset, Resource.Validation}
alias AshAuthentication.{Errors.AuthenticationFailed, Info} alias AshAuthentication.{Errors.AuthenticationFailed, Info}
require Logger require Logger
@doc false @doc false
@impl true @impl true
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()} @spec validate(Changeset.t(), keyword, Validation.Context.t()) :: :ok | {:error, Exception.t()}
def validate(changeset, options) do def validate(changeset, options, _context) do
{:ok, strategy} = get_strategy(changeset, options) {:ok, strategy} = get_strategy(changeset, options)
with {:ok, password_arg} <- get_password_arg(changeset, options, strategy), with {:ok, password_arg} <- get_password_arg(changeset, options, strategy),

View file

@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _opts, _context) do def prepare(query, _opts, _context) do
strategy = Info.strategy_for_action!(query.resource, query.action.name) strategy = Info.strategy_for_action!(query.resource, query.action.name)
@ -28,7 +28,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
select_for_senders = Info.authentication_select_for_senders!(query.resource) select_for_senders = Info.authentication_select_for_senders!(query.resource)
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(^ref(identity_field) == ^identity)
|> Query.before_action(fn query -> |> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders) Ash.Query.ensure_selected(query, select_for_senders)
end) end)

View file

@ -4,13 +4,13 @@ defmodule AshAuthentication.Strategy.Password.ResetTokenValidation do
""" """
use Ash.Resource.Validation use Ash.Resource.Validation
alias Ash.{Changeset, Error.Changes.InvalidArgument} alias Ash.{Changeset, Error.Changes.InvalidArgument, Resource.Validation}
alias AshAuthentication.{Info, Jwt} alias AshAuthentication.{Info, Jwt}
@doc false @doc false
@impl true @impl true
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()} @spec validate(Changeset.t(), keyword, Validation.Context.t()) :: :ok | {:error, Exception.t()}
def validate(changeset, _) do def validate(changeset, _, _) do
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name), with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token), token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token),
{:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource), {:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource),

View file

@ -19,14 +19,14 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, options, context) do def prepare(query, options, context) do
{:ok, strategy} = Info.find_strategy(query, context, options) {:ok, strategy} = Info.find_strategy(query, context, options)
identity_field = strategy.identity_field identity_field = strategy.identity_field
identity = Query.get_argument(query, identity_field) identity = Query.get_argument(query, identity_field)
query query
|> Query.filter(ref(^identity_field) == ^identity) |> Query.filter(^ref(identity_field) == ^identity)
|> check_sign_in_token_configuration(strategy) |> check_sign_in_token_configuration(strategy)
|> Query.before_action(fn query -> |> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, [strategy.hashed_password_field]) Ash.Query.ensure_selected(query, [strategy.hashed_password_field])

View file

@ -12,7 +12,7 @@ defmodule AshAuthentication.Strategy.Password.SignInWithTokenPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, options, context) do def prepare(query, options, context) do
{:ok, strategy} = Info.find_strategy(query, context, options) {:ok, strategy} = Info.find_strategy(query, context, options)

View file

@ -96,7 +96,8 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
with {:ok, resource} <- persisted_option(dsl_state, :module), with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, identity_field), {:ok, attribute} <- find_attribute(dsl_state, identity_field),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]), :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :public?, [true]) do
validate_attribute_unique_constraint(dsl_state, [identity_field], resource) validate_attribute_unique_constraint(dsl_state, [identity_field], resource)
end end
end end
@ -104,8 +105,9 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
defp validate_hashed_password_field(hashed_password_field, dsl_state) do defp validate_hashed_password_field(hashed_password_field, dsl_state) do
with {:ok, resource} <- persisted_option(dsl_state, :module), with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, hashed_password_field), {:ok, attribute} <- find_attribute(dsl_state, hashed_password_field),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
validate_attribute_option(attribute, resource, :sensitive?, [true]) :ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]) do
validate_attribute_option(attribute, resource, :public?, [false])
end end
end end
@ -498,7 +500,8 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
arguments: arguments, arguments: arguments,
changes: changes, changes: changes,
metadata: metadata, metadata: metadata,
accept: [] accept: [],
require_atomic?: false
) )
end end

View file

@ -120,7 +120,7 @@ defprotocol AshAuthentication.Strategy do
See `actions/1` for a list of actions provided by the strategy. See `actions/1` for a list of actions provided by the strategy.
Any options passed to the action will be passed to the underlying `Ash.Api` function. Any options passed to the action will be passed to the underlying `Ash.Domain` function.
""" """
@spec action(t, action, params :: map, options :: keyword) :: @spec action(t, action, params :: map, options :: keyword) ::
:ok | {:ok, Resource.record()} | {:error, any} :ok | {:ok, Resource.record()} | {:error, any}

View file

@ -41,7 +41,7 @@ defmodule AshAuthentication.Supervisor do
raise """ raise """
No otp_app provided to AshAuthentication.Supervisor. No otp_app provided to AshAuthentication.Supervisor.
In order to find your Ash APIs and resources you need to provide the In order to find your Ash domains and resources you need to provide the
name of your OTP application when starting AshAuthentication.Supervisor: name of your OTP application when starting AshAuthentication.Supervisor:
Suggestion, try adding `{AshAuthentication.Supervisor, otp_app: :my_app}` Suggestion, try adding `{AshAuthentication.Supervisor, otp_app: :my_app}`

View file

@ -5,12 +5,13 @@ defmodule AshAuthentication.TokenResource do
%Spark.Dsl.Section{ %Spark.Dsl.Section{
name: :token, name: :token,
describe: "Configuration options for this token resource", describe: "Configuration options for this token resource",
modules: [:api], modules: [:domain],
schema: [ schema: [
api: [ domain: [
type: {:behaviour, Ash.Api}, type: {:behaviour, Ash.Domain},
required: false,
doc: """ doc: """
The Ash API to use to access this resource. The Ash domain to use to access this resource.
""" """
], ],
expunge_expired_action_name: [ expunge_expired_action_name: [
@ -117,11 +118,8 @@ defmodule AshAuthentication.TokenResource do
defmodule MyApp.Accounts.Token do defmodule MyApp.Accounts.Token do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource] extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
token do
api MyApp.Accounts
end
postgres do postgres do
table "tokens" table "tokens"

View file

@ -12,7 +12,7 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec read_expired(Resource.t(), keyword) :: {:ok, [Resource.record()]} | {:error, any} @spec read_expired(Resource.t(), keyword) :: {:ok, [Resource.record()]} | {:error, any}
def read_expired(resource, opts \\ []) do def read_expired(resource, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do {:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do
resource resource
|> Query.new() |> Query.new()
@ -22,7 +22,7 @@ defmodule AshAuthentication.TokenResource.Actions do
} }
}) })
|> Query.for_read(read_expired_action_name, opts) |> Query.for_read(read_expired_action_name, opts)
|> api.read() |> domain.read()
end end
end end
@ -70,7 +70,7 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec token_revoked?(Resource.t(), String.t(), keyword) :: boolean @spec token_revoked?(Resource.t(), String.t(), keyword) :: boolean
def token_revoked?(resource, token, opts \\ []) do def token_revoked?(resource, token, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do {:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
resource resource
|> Query.new() |> Query.new()
@ -80,7 +80,7 @@ defmodule AshAuthentication.TokenResource.Actions do
} }
}) })
|> Query.for_read(is_revoked_action_name, %{"token" => token}, opts) |> Query.for_read(is_revoked_action_name, %{"token" => token}, opts)
|> api.read() |> domain.read()
|> case do |> case do
{:ok, []} -> false {:ok, []} -> false
{:ok, _} -> true {:ok, _} -> true
@ -98,7 +98,7 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec jti_revoked?(Resource.t(), String.t(), keyword) :: boolean @spec jti_revoked?(Resource.t(), String.t(), keyword) :: boolean
def jti_revoked?(resource, jti, opts \\ []) do def jti_revoked?(resource, jti, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do {:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
resource resource
|> Query.new() |> Query.new()
@ -108,7 +108,7 @@ defmodule AshAuthentication.TokenResource.Actions do
} }
}) })
|> Query.for_read(is_revoked_action_name, %{"jti" => jti}, opts) |> Query.for_read(is_revoked_action_name, %{"jti" => jti}, opts)
|> api.read() |> domain.read()
|> case do |> case do
{:ok, []} -> false {:ok, []} -> false
{:ok, _} -> true {:ok, _} -> true
@ -130,7 +130,7 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec revoke(Resource.t(), String.t(), keyword) :: :ok | {:error, any} @spec revoke(Resource.t(), String.t(), keyword) :: :ok | {:error, any}
def revoke(resource, token, opts \\ []) do def revoke(resource, token, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, revoke_token_action_name} <- {:ok, revoke_token_action_name} <-
Info.token_revocation_revoke_token_action_name(resource) do Info.token_revocation_revoke_token_action_name(resource) do
resource resource
@ -145,7 +145,7 @@ defmodule AshAuthentication.TokenResource.Actions do
%{"token" => token}, %{"token" => token},
Keyword.merge(opts, upsert?: true) Keyword.merge(opts, upsert?: true)
) )
|> api.create() |> domain.create()
|> case do |> case do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -161,7 +161,7 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec store_token(Resource.t(), map, keyword) :: :ok | {:error, any} @spec store_token(Resource.t(), map, keyword) :: :ok | {:error, any}
def store_token(resource, params, opts \\ []) do def store_token(resource, params, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, store_token_action_name} <- Info.token_store_token_action_name(resource) do {:ok, store_token_action_name} <- Info.token_store_token_action_name(resource) do
resource resource
|> Changeset.new() |> Changeset.new()
@ -175,7 +175,7 @@ defmodule AshAuthentication.TokenResource.Actions do
params, params,
Keyword.merge(opts, upsert?: true) Keyword.merge(opts, upsert?: true)
) )
|> api.create() |> domain.create()
|> case do |> case do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -189,45 +189,29 @@ defmodule AshAuthentication.TokenResource.Actions do
@spec get_token(Resource.t(), map, keyword) :: {:ok, [Resource.record()]} | {:error, any} @spec get_token(Resource.t(), map, keyword) :: {:ok, [Resource.record()]} | {:error, any}
def get_token(resource, params, opts \\ []) do def get_token(resource, params, opts \\ []) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, get_token_action_name} <- Info.token_get_token_action_name(resource) do {:ok, get_token_action_name} <- Info.token_get_token_action_name(resource) do
resource resource
|> Query.new() |> Query.new()
|> Query.set_context(%{private: %{ash_authentication?: true}}) |> Query.set_context(%{private: %{ash_authentication?: true}})
|> Query.for_read(get_token_action_name, params, opts) |> Query.for_read(get_token_action_name, params, opts)
|> api.read() |> domain.read()
end end
end end
defp expunge_inside_transaction(resource, expunge_expired_action_name, opts) do defp expunge_inside_transaction(resource, expunge_expired_action_name, opts) do
with :ok <- assert_resource_has_extension(resource, TokenResource), with :ok <- assert_resource_has_extension(resource, TokenResource),
{:ok, api} <- Info.token_api(resource), {:ok, domain} <- Info.token_domain(resource),
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource), {:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do
query <- resource
resource |> Query.new() |> Query.set_context(%{private: %{ash_authentication?: true}}), |> Query.new()
query <- Query.for_read(query, read_expired_action_name, opts), |> Query.set_context(%{private: %{ash_authentication?: true}})
{:ok, expired} <- api.read(query) do |> Query.for_read(read_expired_action_name, opts)
Enum.reduce_while(expired, {:ok, []}, fn record, {:ok, notifications} -> |> domain.bulk_destroy(expunge_expired_action_name, %{}, opts)
record |> case do
|> Changeset.new() %{status: :success, notifications: notifications} -> {:ok, notifications}
|> Changeset.set_context(%{ %{errors: errors} -> {:error, Ash.Error.to_class(errors)}
private: %{ end
ash_authentication?: true
}
})
|> Changeset.for_destroy(expunge_expired_action_name, opts)
|> api.destroy(return_notifications?: true)
|> case do
:ok ->
{:cont, {:ok, notifications}}
{:ok, more_notifications} ->
{:cont, {:ok, Enum.concat(notifications, more_notifications)}}
{:error, reason} ->
{:halt, {:error, reason}}
end
end)
end end
end end
end end

View file

@ -8,10 +8,10 @@ defmodule AshAuthentication.TokenResource.Expunger do
```elixir ```elixir
defmodule MyApp.Accounts.Token do defmodule MyApp.Accounts.Token do
use Ash.Resource, use Ash.Resource,
extensions: [AshAuthentication.TokenResource] extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
token do token do
api MyApp.Accounts
expunge_interval 12 expunge_interval 12
end end
end end

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetConfirmationChangesPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _, _) do def prepare(query, _, _) do
jti = Query.get_argument(query, :jti) jti = Query.get_argument(query, :jti)
strategy = query.context.strategy strategy = query.context.strategy

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetTokenPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _, _) do def prepare(query, _, _) do
jti = get_jti(query) jti = get_jti(query)
purpose = Query.get_argument(query, :purpose) purpose = Query.get_argument(query, :purpose)

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.IsRevokedPreparation do
@doc false @doc false
@impl true @impl true
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t() @spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
def prepare(query, _opts, _context) do def prepare(query, _opts, _context) do
case get_jti(query) do case get_jti(query) do
{:ok, jti} -> {:ok, jti} ->

View file

@ -34,13 +34,14 @@ defmodule AshAuthentication.TokenResource.Transformer do
@spec transform(map) :: @spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do def transform(dsl_state) do
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :token), with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :token),
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :jti, :string, maybe_build_attribute(dsl_state, :jti, :string,
primary_key?: true, primary_key?: true,
allow_nil?: false, allow_nil?: false,
sensitive?: true, sensitive?: true,
writable?: true writable?: true,
public?: true
), ),
:ok <- validate_jti_field(dsl_state), :ok <- validate_jti_field(dsl_state),
{:ok, dsl_state} <- {:ok, dsl_state} <-
@ -55,24 +56,26 @@ defmodule AshAuthentication.TokenResource.Transformer do
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :purpose, :string, maybe_build_attribute(dsl_state, :purpose, :string,
allow_nil?: false, allow_nil?: false,
writable?: true writable?: true,
public?: true
), ),
:ok <- validate_purpose_field(dsl_state), :ok <- validate_purpose_field(dsl_state),
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :extra_data, :map, maybe_build_attribute(dsl_state, :extra_data, :map,
allow_nil?: true, allow_nil?: true,
writable?: true writable?: true,
public?: true
), ),
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec, maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec,
allow_nil?: false, allow_nil?: false,
private?: true, public?: false,
default: &DateTime.utc_now/0 default: &DateTime.utc_now/0
), ),
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec, maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
allow_nil?: false, allow_nil?: false,
private?: true, public?: false,
default: &DateTime.utc_now/0, default: &DateTime.utc_now/0,
update_default: &DateTime.utc_now/0 update_default: &DateTime.utc_now/0
), ),
@ -311,11 +314,16 @@ defmodule AshAuthentication.TokenResource.Transformer do
end end
defp build_read_expired_action(_dsl_state, action_name) do defp build_read_expired_action(_dsl_state, action_name) do
import Ash.Filter.TemplateHelpers import Ash.Expr
filter =
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :filter,
filter: expr(expires_at < now())
)
Transformer.build_entity(Resource.Dsl, [:actions], :read, Transformer.build_entity(Resource.Dsl, [:actions], :read,
name: action_name, name: action_name,
filter: expr(expires_at < now()) filters: [filter]
) )
end end
@ -421,7 +429,7 @@ defmodule AshAuthentication.TokenResource.Transformer do
:ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]), :ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]), :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do :ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
validate_attribute_option(attribute, resource, :private?, [false]) validate_attribute_option(attribute, resource, :public?, [true])
end end
end end
@ -439,8 +447,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
with {:ok, resource} <- persisted_option(dsl_state, :module), with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :purpose), {:ok, attribute} <- find_attribute(dsl_state, :purpose),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]), :ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
validate_attribute_option(attribute, resource, :writable?, [true]) :ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
validate_attribute_option(attribute, resource, :public?, [true])
end end
end end
@ -448,8 +457,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
with {:ok, resource} <- persisted_option(dsl_state, :module), with {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, attribute} <- find_attribute(dsl_state, :extra_data), {:ok, attribute} <- find_attribute(dsl_state, :extra_data),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]), :ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]) do :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]),
validate_attribute_option(attribute, resource, :writable?, [true]) :ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
validate_attribute_option(attribute, resource, :public?, [true])
end end
end end
end end

View file

@ -28,28 +28,28 @@ defmodule AshAuthentication.TokenResource.Verifier do
@spec transform(map) :: @spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do def transform(dsl_state) do
validate_api_presence(dsl_state) validate_domain_presence(dsl_state)
end end
defp validate_api_presence(dsl_state) do defp validate_domain_presence(dsl_state) do
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:token], :api), with domain when not is_nil(domain) <- Transformer.get_option(dsl_state, [:token], :domain),
:ok <- assert_is_module(api), :ok <- assert_is_module(domain),
true <- function_exported?(api, :spark_is, 0), true <- function_exported?(domain, :spark_is, 0),
Ash.Api <- api.spark_is() do Ash.Domain <- domain.spark_is() do
{:ok, api} {:ok, domain}
else else
nil -> nil ->
{:error, {:error,
DslError.exception( DslError.exception(
path: [:token, :api], path: [:token, :domain],
message: "An API module must be present" message: "A domain module must be present"
)} )}
_ -> _ ->
{:error, {:error,
DslError.exception( DslError.exception(
path: [:token, :api], path: [:token, :domain],
message: "Module is not an Ash.Api." message: "Module is not an `Ash.Domain`."
)} )}
end end
end end

View file

@ -30,7 +30,7 @@ defmodule AshAuthentication.Transformer do
@spec transform(map) :: @spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do def transform(dsl_state) do
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :authentication), with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :authentication),
:ok <- validate_at_least_one_strategy(dsl_state), :ok <- validate_at_least_one_strategy(dsl_state),
:ok <- validate_unique_strategy_names(dsl_state), :ok <- validate_unique_strategy_names(dsl_state),
:ok <- validate_unique_add_on_names(dsl_state), :ok <- validate_unique_add_on_names(dsl_state),

View file

@ -3,11 +3,12 @@ defmodule AshAuthentication.UserIdentity do
%Spark.Dsl.Section{ %Spark.Dsl.Section{
name: :user_identity, name: :user_identity,
describe: "Configure identity options for this resource", describe: "Configure identity options for this resource",
modules: [:api, :user_resource], modules: [:domain, :user_resource],
schema: [ schema: [
api: [ domain: [
type: {:behaviour, Ash.Api}, type: {:behaviour, Ash.Domain},
doc: "The Ash API to use to access this resource." doc: "The Ash domain to use to access this resource.",
required: false
], ],
user_resource: [ user_resource: [
type: {:behaviour, Ash.Resource}, type: {:behaviour, Ash.Resource},
@ -95,10 +96,10 @@ defmodule AshAuthentication.UserIdentity do
defmodule MyApp.Accounts.UserIdentity do defmodule MyApp.Accounts.UserIdentity do
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.UserIdentity] extensions: [AshAuthentication.UserIdentity],
domain: MyApp.Accounts
user_identity do user_identity do
api MyApp.Accounts
user_resource MyApp.Accounts.User user_resource MyApp.Accounts.User
end end

View file

@ -3,7 +3,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
Code interface for provider identity actions. Code interface for provider identity actions.
Allows you to interact with UserIdentity resources without having to mess Allows you to interact with UserIdentity resources without having to mess
around with changesets, apis, etc. These functions are delegated to from around with changesets, domains, etc. These functions are delegated to from
within `AshAuthentication.UserIdentity`. within `AshAuthentication.UserIdentity`.
""" """
@ -15,7 +15,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
""" """
@spec upsert(Resource.t(), map) :: {:ok, Resource.record()} | {:error, term} @spec upsert(Resource.t(), map) :: {:ok, Resource.record()} | {:error, term}
def upsert(resource, attributes) do def upsert(resource, attributes) do
with {:ok, api} <- UserIdentity.Info.user_identity_api(resource), with {:ok, domain} <- UserIdentity.Info.user_identity_domain(resource),
{:ok, upsert_action_name} <- {:ok, upsert_action_name} <-
UserIdentity.Info.user_identity_upsert_action_name(resource), UserIdentity.Info.user_identity_upsert_action_name(resource),
action when is_map(action) <- Resource.Info.action(resource, upsert_action_name) do action when is_map(action) <- Resource.Info.action(resource, upsert_action_name) do
@ -30,7 +30,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
upsert?: true, upsert?: true,
upsert_identity: action.upsert_identity upsert_identity: action.upsert_identity
) )
|> api.create() |> domain.create()
end end
end end
end end

View file

@ -34,7 +34,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
@spec transform(map) :: @spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do def transform(dsl_state) do
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :user_identity), with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :user_identity),
{:ok, resource} <- persisted_option(dsl_state, :module), {:ok, resource} <- persisted_option(dsl_state, :module),
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, :id, Type.UUID, maybe_build_attribute(dsl_state, :id, Type.UUID,
@ -58,7 +58,8 @@ defmodule AshAuthentication.UserIdentity.Transformer do
{:ok, dsl_state} <- {:ok, dsl_state} <-
maybe_build_attribute(dsl_state, strategy, Type.String, maybe_build_attribute(dsl_state, strategy, Type.String,
allow_nil?: false, allow_nil?: false,
writable?: true writable?: true,
public?: true
), ),
:ok <- validate_strategy_field(dsl_state, strategy), :ok <- validate_strategy_field(dsl_state, strategy),
{:ok, dsl_state} <- {:ok, dsl_state} <-
@ -134,7 +135,8 @@ defmodule AshAuthentication.UserIdentity.Transformer do
{:ok, attribute} <- find_attribute(dsl_state, field_name), {:ok, attribute} <- find_attribute(dsl_state, field_name),
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]), :ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]), :ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do :ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
:ok <- validate_attribute_option(attribute, resource, :public?, [true]) do
:ok :ok
else else
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}
@ -183,7 +185,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
defp build_user_relationship(dsl_state, name, destination) do defp build_user_relationship(dsl_state, name, destination) do
with {:ok, id_attr} <- find_pk(destination), with {:ok, id_attr} <- find_pk(destination),
{:ok, api} <- AshAuthentication.Info.authentication_api(destination), {:ok, domain} <- AshAuthentication.Info.domain(destination),
{:ok, user_id} <- {:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state) do UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state) do
Transformer.build_entity(Resource.Dsl, [:relationships], :belongs_to, Transformer.build_entity(Resource.Dsl, [:relationships], :belongs_to,
@ -193,7 +195,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
destination_attribute: id_attr.name, destination_attribute: id_attr.name,
attribute_type: id_attr.type, attribute_type: id_attr.type,
source_attribute: user_id, source_attribute: user_id,
api: api, domain: domain,
attribute_writable?: true, attribute_writable?: true,
writable?: true writable?: true
) )
@ -202,14 +204,14 @@ defmodule AshAuthentication.UserIdentity.Transformer do
defp validate_user_relationship(dsl_state, name, destination) do defp validate_user_relationship(dsl_state, name, destination) do
with {:ok, id_attr} <- find_pk(destination), with {:ok, id_attr} <- find_pk(destination),
{:ok, api} <- AshAuthentication.Info.authentication_api(destination), {:ok, domain} <- AshAuthentication.Info.domain(destination),
{:ok, relationship} <- find_relationship(dsl_state, name), {:ok, relationship} <- find_relationship(dsl_state, name),
{:ok, user_id} <- {:ok, user_id} <-
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state), UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
:ok <- validate_field_in_values(relationship, :destination, [destination]), :ok <- validate_field_in_values(relationship, :destination, [destination]),
:ok <- validate_field_in_values(relationship, :destination_attribute, [id_attr.name]), :ok <- validate_field_in_values(relationship, :destination_attribute, [id_attr.name]),
:ok <- validate_field_in_values(relationship, :source_attribute, [user_id]), :ok <- validate_field_in_values(relationship, :source_attribute, [user_id]),
:ok <- validate_field_in_values(relationship, :api, [api]) do :ok <- validate_field_in_values(relationship, :domain, [domain]) do
validate_field_in_values(relationship, :attribute_type, [id_attr.type]) validate_field_in_values(relationship, :attribute_type, [id_attr.type])
end end
end end

View file

@ -27,14 +27,14 @@ defmodule AshAuthentication.UserIdentity.Verifier do
@spec transform(map) :: @spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt :ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
def transform(dsl_state) do def transform(dsl_state) do
with :ok <- validate_api_presence(dsl_state) do with :ok <- validate_domain_presence(dsl_state) do
validate_user_resource(dsl_state) validate_user_resource(dsl_state)
end end
end end
defp validate_api_presence(dsl_state) do defp validate_domain_presence(dsl_state) do
with {:ok, api} <- Info.user_identity_api(dsl_state) do with {:ok, domain} <- Info.user_identity_domain(dsl_state) do
assert_is_api(api) assert_is_domain(domain)
end end
end end

View file

@ -1,6 +1,6 @@
defmodule AshAuthentication.Utils do defmodule AshAuthentication.Utils do
@moduledoc false @moduledoc false
alias Ash.{Api, Resource} alias Ash.{Domain, Resource}
alias Spark.{Dsl, Dsl.Transformer} alias Spark.{Dsl, Dsl.Transformer}
@doc """ @doc """
@ -80,13 +80,13 @@ defmodule AshAuthentication.Utils do
def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements) def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements)
@doc """ @doc """
Used within transformers to infer `api` from a resource if the option is not set. Used within transformers to infer `domain` from a resource if the option is not set.
""" """
def maybe_set_api(dsl_state, section) do def maybe_set_domain(dsl_state, section) do
api = Transformer.get_persisted(dsl_state, :api) domain = Transformer.get_persisted(dsl_state, :domain)
if api && !Transformer.get_option(dsl_state, [section], :api) do if domain && !Transformer.get_option(dsl_state, [section], :domain) do
{:ok, Transformer.set_option(dsl_state, [section], :api, api)} {:ok, Transformer.set_option(dsl_state, [section], :domain, domain)}
else else
{:ok, dsl_state} {:ok, dsl_state}
end end
@ -209,16 +209,16 @@ defmodule AshAuthentication.Utils do
end end
@doc """ @doc """
Asserts that `module` is actually an Ash API. Asserts that `module` is actually an Ash domain.
""" """
@spec assert_is_api(Api.t()) :: :ok | {:error, term} @spec assert_is_domain(Domain.t()) :: :ok | {:error, term}
def assert_is_api(module) do def assert_is_domain(module) do
with :ok <- assert_is_module(module), with :ok <- assert_is_module(module),
true <- function_exported?(module, :spark_is, 0), true <- function_exported?(module, :spark_is, 0),
Api <- module.spark_is() do Domain <- module.spark_is() do
:ok :ok
else else
_ -> {:error, "Module `#{inspect(module)}` is not an Ash API"} _ -> {:error, "Module `#{inspect(module)}` is not an Ash domain"}
end end
end end

View file

@ -198,4 +198,52 @@ defmodule AshAuthentication.Validations.Action do
"The action `#{inspect(action.name)}` should have the `#{inspect(preparation_module)}` preparation present." "The action `#{inspect(action.name)}` should have the `#{inspect(preparation_module)}` preparation present."
)} )}
end end
@doc """
Validate the action has the provided option.
"""
@spec validate_action_option(Actions.action(), atom, [any]) :: :ok | {:error, Exception.t()}
def validate_action_option(action, field, values) do
with {:ok, value} <- Map.fetch(action, field),
true <- value in values do
:ok
else
:error ->
{:error,
DslError.exception(
path: [:actions, action.name, field],
message:
"The action `#{inspect(action.name)}` is missing the `#{inspect(field)}` option set"
)}
false ->
case values do
[] ->
{:error,
DslError.exception(
path: [:actions, action.name, field],
message:
"The action `#{inspect(action.name)}` should not have the `#{inspect(field)}` option set"
)}
[expected] ->
{:error,
DslError.exception(
path: [:actions, action.name, field],
message:
"The action `#{inspect(action.name)}` should have the `#{inspect(field)}` option set to `#{inspect(expected)}`"
)}
expected ->
expected = expected |> Enum.map(&"`#{inspect(&1)}`") |> to_sentence(final: "or")
{:error,
DslError.exception(
path: [:actions, action.name, field],
message:
"The action `#{inspect(action.name)}` should have the `#{inspect(field)}` option set to one of #{expected}"
)}
end
end
end
end end

View file

@ -19,6 +19,7 @@ defmodule AshAuthentication.Validations.Attribute do
:error -> :error ->
{:error, {:error,
DslError.exception( DslError.exception(
module: resource,
path: [:actions, :attribute], path: [:actions, :attribute],
message: message:
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is missing the `#{inspect(field)}` property" "The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is missing the `#{inspect(field)}` property"
@ -29,6 +30,7 @@ defmodule AshAuthentication.Validations.Attribute do
[] -> [] ->
{:error, {:error,
DslError.exception( DslError.exception(
module: resource,
path: [:actions, :attribute], path: [:actions, :attribute],
message: message:
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is should not have `#{inspect(field)}` set" "The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is should not have `#{inspect(field)}` set"
@ -37,6 +39,7 @@ defmodule AshAuthentication.Validations.Attribute do
[expected] -> [expected] ->
{:error, {:error,
DslError.exception( DslError.exception(
module: resource,
path: [:actions, :attribute], path: [:actions, :attribute],
message: message:
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to `#{inspect(expected)}`" "The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to `#{inspect(expected)}`"
@ -47,6 +50,7 @@ defmodule AshAuthentication.Validations.Attribute do
{:error, {:error,
DslError.exception( DslError.exception(
module: resource,
path: [:actions, :attribute], path: [:actions, :attribute],
message: message:
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to one of #{expected}" "The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to one of #{expected}"

View file

@ -17,30 +17,31 @@ defmodule AshAuthentication.Verifier do
| {:error, term} | {:error, term}
| {:warn, String.t() | list(String.t())} | {:warn, String.t() | list(String.t())}
def verify(dsl_state) do def verify(dsl_state) do
with {:ok, _api} <- validate_api_presence(dsl_state) do with {:ok, _domain} <- validate_domain_presence(dsl_state) do
validate_token_resource(dsl_state) validate_token_resource(dsl_state)
end end
end end
defp validate_api_presence(dsl_state) do defp validate_domain_presence(dsl_state) do
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:authentication], :api), with domain when not is_nil(domain) <-
:ok <- assert_is_module(api), Transformer.get_option(dsl_state, [:authentication], :domain),
true <- function_exported?(api, :spark_is, 0), :ok <- assert_is_module(domain),
Ash.Api <- api.spark_is() do true <- function_exported?(domain, :spark_is, 0),
{:ok, api} Ash.Domain <- domain.spark_is() do
{:ok, domain}
else else
nil -> nil ->
{:error, {:error,
DslError.exception( DslError.exception(
path: [:authentication, :api], path: [:authentication, :domain],
message: "An API module must be present" message: "A domain module must be present"
)} )}
_ -> _ ->
{:error, {:error,
DslError.exception( DslError.exception(
path: [:authentication, :api], path: [:authentication, :domain],
message: "Module is not an Ash.Api." message: "Module is not an `Ash.Domain`."
)} )}
end end
end end

13
mix.exs
View file

@ -173,7 +173,7 @@ defmodule AshAuthentication.MixProject do
# Run "mix help deps" to learn about dependencies. # Run "mix help deps" to learn about dependencies.
defp deps do defp deps do
[ [
{:ash, ash_version("~> 2.5 and >= 2.5.11")}, {:ash, ash_version("== 3.0.0-rc.1")},
{:assent, "~> 0.2 and >= 0.2.8"}, {:assent, "~> 0.2 and >= 0.2.8"},
{:bcrypt_elixir, "~> 3.0"}, {:bcrypt_elixir, "~> 3.0"},
{:castore, "~> 1.0"}, {:castore, "~> 1.0"},
@ -181,16 +181,17 @@ defmodule AshAuthentication.MixProject do
{:jason, "~> 1.4"}, {:jason, "~> 1.4"},
{:joken, "~> 2.5"}, {:joken, "~> 2.5"},
{:plug, "~> 1.13"}, {:plug, "~> 1.13"},
{:spark, "~> 1.1 and >= 1.1.39"}, {:spark, "~> 2.0"},
{:splode, "~> 0.2"},
{:absinthe_plug, "~> 1.5", only: [:dev, :test]}, {:absinthe_plug, "~> 1.5", only: [:dev, :test]},
{:ash_graphql, "~> 0.21", only: [:dev, :test]}, # {:ash_graphql, "~> 0.21", only: [:dev, :test]},
{:ash_json_api, "~> 0.30", only: [:dev, :test]}, # {:ash_json_api, "~> 0.30", only: [:dev, :test]},
{:ash_postgres, "~> 1.5.1", optional: true}, {:ash_postgres, "== 2.0.0-rc.1", optional: true},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}, {:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false}, {:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
{:doctor, "~> 0.18", only: [:dev, :test]}, {:doctor, "~> 0.18", only: [:dev, :test]},
{:ex_check, "~> 0.15", only: [:dev, :test]}, {:ex_check, "~> 0.15", only: [:dev, :test]},
{:ex_doc, github: "elixir-lang/ex_doc", only: [:dev, :test], runtime: false}, {:ex_doc, ">= 0.0.0", only: [:dev, :test]},
{:faker, "~> 0.18.0", only: [:dev, :test]}, {:faker, "~> 0.18.0", only: [:dev, :test]},
{:git_ops, "~> 2.4", only: [:dev, :test], runtime: false}, {:git_ops, "~> 2.4", only: [:dev, :test], runtime: false},
{:mimic, "~> 1.7", only: [:dev, :test]}, {:mimic, "~> 1.7", only: [:dev, :test]},

View file

@ -1,17 +1,14 @@
%{ %{
"absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0 or ~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"}, "absinthe": {:hex, :absinthe, "1.7.6", "0b897365f98d068cfcb4533c0200a8e58825a4aeeae6ec33633ebed6de11773b", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7626951ca5eec627da960615b51009f3a774765406ff02722b1d818f17e5778"},
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"}, "absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
"ash": {:hex, :ash, "2.21.2", "d62657fc18ee8a519042b03721ab34427ed640d0dbd1ddd79df175dd2876b8f6", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.6", [hex: :reactor, repo: "hexpm", optional: false]}, {:spark, ">= 1.1.55 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b512c51812ef971c1cb2993b55ffc9aa833e251cdbbdbd813657e9533e78c3d9"}, "ash": {:hex, :ash, "3.0.0-rc.1", "ab922a15d85f4c2ee2250908239cf8dda3ffdbdcac4664b7814afdd024428046", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.8", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.7 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf3e579867f3ed15e219789fe47965473b1639aae68c383579fbc4a982e11f85"},
"ash_graphql": {:hex, :ash_graphql, "0.27.1", "514ea4d3f2dafab45d6d4a0c7eb8037719a693a905295575f1faa069b6791b09", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, "~> 2.17", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "81585f7db55730938f263f78d32e35cea81b6d1139b14ce59cdf59e1ab3a688b"}, "ash_postgres": {:hex, :ash_postgres, "2.0.0-rc.1", "c6f2284ab5c7271df63cf13af82655c8632d32665809d2120861c38bd9ee32b3", [:mix], [{:ash, "~> 3.0.0-rc.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:simple_sat, "~> 0.1", [hex: :simple_sat, repo: "hexpm", optional: false]}], "hexpm", "840d95c9ac9e363620428568c22a0312aaa181c74f55289a8bc6588801f22e93"},
"ash_json_api": {:hex, :ash_json_api, "0.34.2", "21a1f935d1208d7f419f08cb44ae379ffa9919dc4860e6bbc6e7499762986e7e", [:mix], [{:ash, ">= 2.9.24 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4.0", [hex: :json_xema, repo: "hexpm", optional: false]}, {:open_api_spex, "~> 3.16", [hex: :open_api_spex, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "620658e495ac745807d8eab0e752836f44e1368c98c7beaad5d4c2bd8c286cf4"},
"ash_postgres": {:hex, :ash_postgres, "1.5.22", "3cad63ffce8080615240f01caf389876dbbadda61a298585d6dc50fa4d48680f", [:mix], [{:ash, ">= 2.20.3 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "1056587a9e6aab2b0ed282a9ab70a7e5f3a059eed7c7546e0265225ad62f41fc"},
"assent": {:hex, :assent, "0.2.9", "e3cdbc8f2e4f8d02c4c490ef8c2148bb1bc0d81aa0648f09addc5918d9a1cd5a", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "5f9562bda90bef7bd3f1b9a348520a5631b86c85145346bb7edb8a7ebbad8e86"}, "assent": {:hex, :assent, "0.2.9", "e3cdbc8f2e4f8d02c4c490ef8c2148bb1bc0d81aa0648f09addc5918d9a1cd5a", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "5f9562bda90bef7bd3f1b9a348520a5631b86c85145346bb7edb8a7ebbad8e86"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"}, "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"}, "comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
@ -20,7 +17,6 @@
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"}, "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"}, "doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"}, "ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
@ -28,7 +24,7 @@
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"}, "ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "a663c13478a49d29ae0267b6e45badb803267cf0", []}, "ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"}, "faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
@ -38,32 +34,31 @@
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"}, "joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
"json_xema": {:hex, :json_xema, "0.4.2", "85de190f597a98ce9da436b8a59c97ef561a6ab6017255df8b494babefd6fb10", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.11", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "5516213758667d21669e0d63ea287238d277519527bac6c02140a5e34c1fda80"},
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"}, "mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"},
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"}, "mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"}, "nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"}, "postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"reactor": {:hex, :reactor, "0.7.0", "fb76d23d95829b28ac9b9d654620c43c890c6a32ea26ac13086c48540b34e8c5", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 1.0", [hex: :spark, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4310da820d753aafd7dc4ee8cc687b84565dd6d9536e38806ee211da792178fd"}, "reactor": {:hex, :reactor, "0.8.1", "1aec71d16083901277727c8162f6dd0f07e80f5ca98911b6ef4f2c95e6e62758", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae3936d97a3e4a316744f70c77b85345b08b70da334024c26e6b5eb8ede1246b"},
"simple_sat": {:hex, :simple_sat, "0.1.1", "68a5ebe6f6d5956bd806e4881c495692c14580a2f1a4420488985abd0fba2119", [:mix], [], "hexpm", "63571218f92ff029838df7645eb8f0c38df8ed60d2d14578412a8d142a94471e"},
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"}, "sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
"spark": {:hex, :spark, "1.1.55", "d20c3f899b23d841add29edc912ffab4463d3bb801bc73448738631389291d2e", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "bbc15a4223d8e610c81ceca825d5d0bae3738d1c4ac4dbb1061749966776c3f1"}, "spark": {:hex, :spark, "2.1.8", "406256443d5e23ec034a0520c5bee703385ce0840825194aa583c96c22c2a349", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "cc46f7b3d31efe7995d6348e1664b1e18d5193761ad5462d61059078578d5f4c"},
"splode": {:hex, :splode, "0.2.0", "a1f3b5a8e7c957be495bf0f22dd9e0567a87ec63559963a0ce0c3f0e8dfacedc", [:mix], [], "hexpm", "7cfecc5913ff7feeb04f143e2494cfa7bc6d5bb5bec70f7ffac94c18ea97f303"},
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"}, "typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
"xema": {:hex, :xema, "0.17.1", "fa83ed90ec7d9a5e38a223ee1f0693cfb8cd3fa0d0c7f7967f828a0643811f10", [:mix], [{:conv_case, "~> 0.2.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "3dd7213309cc8e6d7770ee54de807a0d91cdbdd9dcb78a6f3eee9dbad43889af"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
} }

View file

@ -0,0 +1,21 @@
defmodule Example.Repo.Migrations.Install2ExtensionsWat do
@moduledoc """
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
execute("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
execute("CREATE EXTENSION IF NOT EXISTS \"citext\"")
end
def down do
# Uncomment this if you actually want to uninstall the extensions
# when this migration is rolled back:
# execute("DROP EXTENSION IF EXISTS \"uuid-ossp\"")
# execute("DROP EXTENSION IF EXISTS \"citext\"")
end
end

View file

@ -0,0 +1,55 @@
defmodule Example.Repo.Migrations.UpdateTimestampDefaults do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:user_with_token_required) do
modify(:updated_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
modify(:created_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
modify(:id, :uuid, default: fragment("gen_random_uuid()"))
end
alter table(:user_identities) do
modify(:id, :uuid, default: fragment("gen_random_uuid()"))
end
alter table(:user) do
modify(:updated_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
modify(:created_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
modify(:id, :uuid, default: fragment("gen_random_uuid()"))
end
alter table(:tokens) do
modify(:created_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
modify(:updated_at, :utc_datetime_usec, default: fragment("(now() AT TIME ZONE 'utc')"))
end
end
def down do
alter table(:tokens) do
modify(:updated_at, :utc_datetime_usec, default: fragment("now()"))
modify(:created_at, :utc_datetime_usec, default: fragment("now()"))
end
alter table(:user) do
modify(:id, :uuid, default: fragment("uuid_generate_v4()"))
modify(:created_at, :utc_datetime_usec, default: fragment("now()"))
modify(:updated_at, :utc_datetime_usec, default: fragment("now()"))
end
alter table(:user_identities) do
modify(:id, :uuid, default: fragment("uuid_generate_v4()"))
end
alter table(:user_with_token_required) do
modify(:id, :uuid, default: fragment("uuid_generate_v4()"))
modify(:created_at, :utc_datetime_usec, default: fragment("now()"))
modify(:updated_at, :utc_datetime_usec, default: fragment("now()"))
end
end
end

View file

@ -0,0 +1,89 @@
{
"attributes": [
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "updated_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "created_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "map",
"source": "extra_data",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "purpose",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "utc_datetime",
"source": "expires_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "subject",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "jti",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": true
}
],
"table": "tokens",
"hash": "AE2F09DE877864EE3A814D7B2D24A41B88B79C869BEAD92907E6A980301E7ADC",
"repo": "Elixir.Example.Repo",
"identities": [],
"schema": null,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true
}

View file

@ -0,0 +1,109 @@
{
"attributes": [
{
"default": "nil",
"size": null,
"type": "utc_datetime_usec",
"source": "confirmed_at",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"gen_random_uuid()\")",
"size": null,
"type": "uuid",
"source": "id",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": true
},
{
"default": "nil",
"size": null,
"type": "citext",
"source": "username",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "extra_stuff",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "not_accepted_extra_stuff",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "hashed_password",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "created_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "updated_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
}
],
"table": "user",
"hash": "2B26A24059722F798F9921CCB4E7E954478B920107F240EF313EE35CCCCEC1F4",
"repo": "Elixir.Example.Repo",
"identities": [
{
"name": "username",
"keys": [
"username"
],
"all_tenants?": false,
"index_name": "user_username_index",
"base_filter": null
}
],
"schema": null,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true
}

View file

@ -0,0 +1,119 @@
{
"attributes": [
{
"default": "nil",
"size": null,
"type": "text",
"source": "refresh_token",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "utc_datetime_usec",
"source": "access_token_expires_at",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "access_token",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "uid",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "strategy",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"gen_random_uuid()\")",
"size": null,
"type": "uuid",
"source": "id",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": true
},
{
"default": "nil",
"size": null,
"type": "uuid",
"source": "user_id",
"references": {
"name": "user_identities_user_id_fkey",
"table": "user",
"schema": "public",
"on_delete": null,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"primary_key?": true,
"destination_attribute": "id",
"deferrable": false,
"match_type": null,
"match_with": null,
"on_update": null,
"destination_attribute_default": null,
"destination_attribute_generated": null
},
"allow_nil?": true,
"generated?": false,
"primary_key?": false
}
],
"table": "user_identities",
"hash": "9ED472E910FD711BEFFEAAACF1199466AB34B7FF41C5C076F1A95188FC3D60C6",
"repo": "Elixir.Example.Repo",
"identities": [
{
"name": "unique_on_strategy_and_uid_and_user_id",
"keys": [
"strategy",
"uid",
"user_id"
],
"all_tenants?": false,
"index_name": "user_identities_unique_on_strategy_and_uid_and_user_id_index",
"base_filter": null
}
],
"schema": null,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true
}

View file

@ -0,0 +1,79 @@
{
"attributes": [
{
"default": "fragment(\"gen_random_uuid()\")",
"size": null,
"type": "uuid",
"source": "id",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": true
},
{
"default": "nil",
"size": null,
"type": "citext",
"source": "email",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "nil",
"size": null,
"type": "text",
"source": "hashed_password",
"references": null,
"allow_nil?": true,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "created_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
},
{
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
"size": null,
"type": "utc_datetime_usec",
"source": "updated_at",
"references": null,
"allow_nil?": false,
"generated?": false,
"primary_key?": false
}
],
"table": "user_with_token_required",
"hash": "A677D02AFEED48B85D1622FAC059A1D8B0B4C510F78ED44AF53A0F6649E12F6A",
"repo": "Elixir.Example.Repo",
"identities": [
{
"name": "email",
"keys": [
"email"
],
"all_tenants?": false,
"index_name": "user_with_token_required_email_index",
"base_filter": null
}
],
"schema": null,
"multitenancy": {
"global": null,
"strategy": null,
"attribute": null
},
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true
}

View file

@ -33,6 +33,7 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
test "it updates the confirmed_at field" do test "it updates the confirmed_at field" do
{:ok, strategy} = Info.strategy(Example.User, :confirm) {:ok, strategy} = Info.strategy(Example.User, :confirm)
user = build_user() user = build_user()
new_username = username() new_username = username()
@ -45,11 +46,16 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, []) assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, [])
assert confirmed_user.id == user.id assert confirmed_user.id == user.id
assert to_string(confirmed_user.username) == new_username
assert_in_delta DateTime.to_unix(confirmed_user.confirmed_at), assert_in_delta DateTime.to_unix(confirmed_user.confirmed_at),
DateTime.to_unix(DateTime.utc_now()), DateTime.to_unix(DateTime.utc_now()),
1.0 1.0
# I don't know why this is failing. I even tried changing
# `AshAuthentication.AddOn.Confirmation.ConfirmChange` to use
# `Ash.Changeset.force_change_attributes/2` to no avail.
# I can see the updated_at being set, but not the new username.
assert to_string(confirmed_user.username) == new_username
end end
end end

View file

@ -122,33 +122,6 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
assert claims["sub"] =~ "user?id=#{user.id}" assert claims["sub"] =~ "user?id=#{user.id}"
end end
test "it cant set unaccepted fields" do
{:ok, strategy} = Info.strategy(Example.User, :password)
username = username()
password = password()
assert {:error,
%Ash.Error.Invalid{
errors: [
%Ash.Error.Changes.InvalidAttribute{
message: "cannot be changed",
field: :not_accepted_extra_stuff
}
]
}} =
Actions.register(
strategy,
%{
"username" => username,
"password" => password,
"password_confirmation" => password,
"not_accepted_extra_stuff" => "Extra"
},
[]
)
end
test "it returns an error if the user already exists" do test "it returns an error if the user already exists" do
user = build_user() user = build_user()
{:ok, strategy} = Info.strategy(Example.User, :password) {:ok, strategy} = Info.strategy(Example.User, :password)
@ -208,7 +181,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
capture_log(fn -> capture_log(fn ->
params = %{"username" => user.username} params = %{"username" => user.username}
options = [] options = []
api = Info.authentication_api!(strategy.resource) domain = Info.domain!(strategy.resource)
resettable = strategy.resettable resettable = strategy.resettable
result = result =
@ -221,7 +194,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
}) })
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params) |> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|> Ash.Query.select([]) |> Ash.Query.select([])
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, _} -> :ok {:ok, _} -> :ok
{:error, reason} -> {:error, reason} {:error, reason} -> {:error, reason}

View file

@ -16,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
} }
{:ok, _user, _changeset, _} = {:ok, _user, _changeset, _} =
Changeset.new(strategy.resource, %{}) Changeset.new(strategy.resource)
|> Changeset.for_create(strategy.register_action_name, attrs) |> Changeset.for_create(strategy.register_action_name, attrs)
|> HashPasswordChange.change([], %{}) |> HashPasswordChange.change([], %{})
|> Changeset.with_hooks(fn changeset -> |> Changeset.with_hooks(fn changeset ->
@ -37,7 +37,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
} }
{:ok, _user, _changeset, _} = {:ok, _user, _changeset, _} =
Changeset.new(user, %{}) Changeset.new(user)
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)}) |> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|> Changeset.for_update(:update, attrs) |> Changeset.for_update(:update, attrs)
|> HashPasswordChange.change([], %{}) |> HashPasswordChange.change([], %{})
@ -59,7 +59,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
} }
{:ok, _user, _changeset, _} = {:ok, _user, _changeset, _} =
Changeset.new(user, %{}) Changeset.new(user)
|> Changeset.for_update(:update, attrs) |> Changeset.for_update(:update, attrs)
|> HashPasswordChange.change([], %{strategy_name: Strategy.name(strategy)}) |> HashPasswordChange.change([], %{strategy_name: Strategy.name(strategy)})
|> Changeset.with_hooks(fn changeset -> |> Changeset.with_hooks(fn changeset ->
@ -80,7 +80,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
} }
{:ok, _user, _changeset, _} = {:ok, _user, _changeset, _} =
Changeset.new(user, %{}) Changeset.new(user)
|> Changeset.for_update(:update, attrs) |> Changeset.for_update(:update, attrs)
|> HashPasswordChange.change([strategy_name: :password], %{}) |> HashPasswordChange.change([strategy_name: :password], %{})
|> Changeset.with_hooks(fn changeset -> |> Changeset.with_hooks(fn changeset ->

View file

@ -16,9 +16,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidationTest
} }
assert {:error, %InvalidArgument{field: :password_confirmation}} = assert {:error, %InvalidArgument{field: :password_confirmation}} =
Changeset.new(strategy.resource, %{}) Changeset.new(strategy.resource)
|> Changeset.for_create(strategy.register_action_name, attrs) |> Changeset.for_create(strategy.register_action_name, attrs)
|> PasswordConfirmationValidation.validate([]) |> PasswordConfirmationValidation.validate([], %{})
end end
end end
@ -33,9 +33,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidationTest
} }
assert {:error, %InvalidArgument{field: :password_confirmation}} = assert {:error, %InvalidArgument{field: :password_confirmation}} =
Changeset.new(user, %{}) Changeset.new(user)
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)}) |> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|> Changeset.for_update(:update, attrs) |> Changeset.for_update(:update, attrs)
|> PasswordConfirmationValidation.validate([]) |> PasswordConfirmationValidation.validate([], %{})
end end
end end

View file

@ -10,11 +10,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidationTest do
assert :ok = assert :ok =
user user
|> Changeset.new(%{}) |> Changeset.new()
|> Changeset.set_argument(:current_password, user.__metadata__.password) |> Changeset.set_argument(:current_password, user.__metadata__.password)
|> PasswordValidation.validate( |> PasswordValidation.validate(
strategy_name: :password, [
password_argument: :current_password strategy_name: :password,
password_argument: :current_password
],
%{}
) )
end end
@ -23,11 +26,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidationTest do
assert {:error, %AuthenticationFailed{field: :current_password}} = assert {:error, %AuthenticationFailed{field: :current_password}} =
user user
|> Changeset.new(%{}) |> Changeset.new()
|> Changeset.set_argument(:current_password, password()) |> Changeset.set_argument(:current_password, password())
|> PasswordValidation.validate( |> PasswordValidation.validate(
strategy_name: :password, [
password_argument: :current_password strategy_name: :password,
password_argument: :current_password
],
%{}
) )
end end
end end

View file

@ -85,7 +85,7 @@ defmodule DataCase do
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Ash.Changeset.for_create(:register_with_password, attrs) |> Ash.Changeset.for_create(:register_with_password, attrs)
|> Ash.Changeset.force_change_attributes(force_change_attrs) |> Ash.Changeset.force_change_attributes(force_change_attrs)
|> Example.create!() |> Ash.create!()
attrs attrs
|> Enum.reduce(user, fn {field, value}, user -> |> Enum.reduce(user, fn {field, value}, user ->
@ -109,7 +109,7 @@ defmodule DataCase do
Example.UserWithTokenRequired Example.UserWithTokenRequired
|> Ash.Changeset.new() |> Ash.Changeset.new()
|> Ash.Changeset.for_create(:register_with_password, attrs) |> Ash.Changeset.for_create(:register_with_password, attrs)
|> Example.create!() |> Ash.create!()
attrs attrs
|> Enum.reduce(user, fn {field, value}, user -> |> Enum.reduce(user, fn {field, value}, user ->

View file

@ -1,6 +1,7 @@
defmodule Example do defmodule Example do
@moduledoc false @moduledoc false
use Ash.Api, otp_app: :ash_authentication, extensions: [AshGraphql.Api, AshJsonApi.Api] use Ash.Domain, otp_app: :ash_authentication
# , extensions: [AshGraphql.Api, AshJsonApi.Api]
resources do resources do
resource Example.User resource Example.User
@ -9,7 +10,7 @@ defmodule Example do
resource Example.UserIdentity resource Example.UserIdentity
end end
json_api do # json_api do
prefix "/api" # prefix "/api"
end # end
end end

View file

@ -54,6 +54,7 @@ defmodule Example.OnlyMartiesAtTheParty do
alias AshAuthentication.Errors.AuthenticationFailed alias AshAuthentication.Errors.AuthenticationFailed
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2] import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
require Ash.Query require Ash.Query
import Ash.Expr
def name(strategy), do: strategy.name def name(strategy), do: strategy.name
@ -79,18 +80,18 @@ defmodule Example.OnlyMartiesAtTheParty do
def action(strategy, :sign_in, params, options) do def action(strategy, :sign_in, params, options) do
name_field = strategy.name_field name_field = strategy.name_field
name = Map.get(params, to_string(name_field)) name = Map.get(params, to_string(name_field))
api = AshAuthentication.Info.authentication_api!(strategy.resource) domain = AshAuthentication.Info.domain!(strategy.resource)
strategy.resource strategy.resource
|> Ash.Query.filter(ref(^name_field) == ^name) |> Ash.Query.filter(expr(^ref(name_field) == ^name))
|> then(fn query -> |> then(fn query ->
if strategy.case_sensitive? do if strategy.case_sensitive? do
Ash.Query.filter(query, like(ref(^name_field), "Marty%")) Ash.Query.filter(query, like(^ref(name_field), "Marty%"))
else else
Ash.Query.filter(query, ilike(ref(^name_field), "Marty%")) Ash.Query.filter(query, ilike(^ref(name_field), "Marty%"))
end end
end) end)
|> api.read(options) |> domain.read(options)
|> case do |> case do
{:ok, [user]} -> {:ok, [user]} ->
{:ok, user} {:ok, user}

View file

@ -1,11 +1,9 @@
defmodule Example.Schema do # defmodule Example.Schema do
@moduledoc false # @moduledoc false
use Absinthe.Schema # use Absinthe.Schema
@apis [Example] # use AshGraphql, apis: [Example]
use AshGraphql, apis: @apis # query do
# end
query do # end
end
end

View file

@ -2,14 +2,11 @@ defmodule Example.Token do
@moduledoc false @moduledoc false
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource] extensions: [AshAuthentication.TokenResource],
domain: Example
postgres do postgres do
table("tokens") table("tokens")
repo(Example.Repo) repo(Example.Repo)
end end
token do
api Example
end
end end

View file

@ -4,10 +4,11 @@ defmodule Example.User do
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [ extensions: [
AshAuthentication, AshAuthentication,
AshGraphql.Resource, # AshGraphql.Resource,
AshJsonApi.Resource, # AshJsonApi.Resource,
Example.OnlyMartiesAtTheParty Example.OnlyMartiesAtTheParty
] ],
domain: Example
require Logger require Logger
@ -22,10 +23,10 @@ defmodule Example.User do
attributes do attributes do
uuid_primary_key :id, writable?: true uuid_primary_key :id, writable?: true
attribute :username, :ci_string, allow_nil?: false attribute :username, :ci_string, allow_nil?: false, public?: true
attribute :extra_stuff, :string attribute :extra_stuff, :string, public?: true
attribute :not_accepted_extra_stuff, :string attribute :not_accepted_extra_stuff, :string
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, private?: true attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, public?: false
create_timestamp :created_at create_timestamp :created_at
update_timestamp :updated_at update_timestamp :updated_at
@ -48,7 +49,9 @@ defmodule Example.User do
update :update do update :update do
argument :password, :string, allow_nil?: true, sensitive?: true argument :password, :string, allow_nil?: true, sensitive?: true
argument :password_confirmation, :string, allow_nil?: true, sensitive?: true argument :password_confirmation, :string, allow_nil?: true, sensitive?: true
accept [:username]
primary? true primary? true
require_atomic? false
end end
create :register_with_auth0 do create :register_with_auth0 do
@ -113,35 +116,34 @@ defmodule Example.User do
end end
code_interface do code_interface do
define_for Example
define :update_user, action: :update define :update_user, action: :update
end end
graphql do # graphql do
type :user # type :user
queries do # queries do
get :get_user, :read # get :get_user, :read
list :list_users, :read # list :list_users, :read
read_one :current_user, :current_user # read_one :current_user, :current_user
end # end
mutations do # mutations do
create :register, :register_with_password # create :register, :register_with_password
end # end
end # end
json_api do # json_api do
type "user" # type "user"
routes do # routes do
base "/users" # base "/users"
get :read # get :read
get :current_user, route: "/me" # get :current_user, route: "/me"
index :read # index :read
post :register_with_password # post :register_with_password
end # end
end # end
postgres do postgres do
table "user" table "user"
@ -149,8 +151,6 @@ defmodule Example.User do
end end
authentication do authentication do
api Example
select_for_senders([:username]) select_for_senders([:username])
tokens do tokens do

View file

@ -2,10 +2,10 @@ defmodule Example.UserIdentity do
@moduledoc false @moduledoc false
use Ash.Resource, use Ash.Resource,
data_layer: AshPostgres.DataLayer, data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.UserIdentity] extensions: [AshAuthentication.UserIdentity],
domain: Example
user_identity do user_identity do
api Example
user_resource(Example.User) user_resource(Example.User)
end end

View file

@ -1,6 +1,10 @@
defmodule Example.UserWithTokenRequired do defmodule Example.UserWithTokenRequired do
@moduledoc false @moduledoc false
use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication] use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication],
domain: Example
require Logger require Logger
@type t :: %__MODULE__{ @type t :: %__MODULE__{
@ -13,15 +17,13 @@ defmodule Example.UserWithTokenRequired do
attributes do attributes do
uuid_primary_key :id, writable?: true uuid_primary_key :id, writable?: true
attribute :email, :ci_string, allow_nil?: false attribute :email, :ci_string, allow_nil?: false, public?: true
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, private?: true attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, public?: false
create_timestamp :created_at create_timestamp :created_at
update_timestamp :updated_at update_timestamp :updated_at
end end
authentication do authentication do
api Example
tokens do tokens do
enabled? true enabled? true
store_all_tokens? true store_all_tokens? true