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 = [
access_token_attribute_name: 1,
access_token_expires_at_attribute_name: 1,
api: 1,
auth0: 0,
auth0: 1,
auth0: 2,
@ -21,6 +20,7 @@ spark_locals_without_parens = [
confirmation_required?: 1,
confirmed_at_field: 1,
destroy_action_name: 1,
domain: 1,
enabled?: 1,
expunge_expired_action_name: 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: [
"*.{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, ecto_repos: [Example.Repo], ash_apis: [Example]
config :ash_authentication, ecto_repos: [Example.Repo], ash_domains: [Example]
config :ash_authentication, Example.Repo,
username: "postgres",

View file

@ -1,6 +1,6 @@
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,
username: "postgres",

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,7 @@
<h2>Resources:</h2>
<%= 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 %>

View file

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

View file

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

View file

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

View file

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

View file

@ -30,11 +30,8 @@ system to function.
defmodule MyApp.Accounts.Token do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource]
token do
api MyApp.Accounts
end
extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
postgres do
table "tokens"
@ -70,7 +67,7 @@ Configuration options for this token resource
| 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. |
| [`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. |

View file

@ -29,10 +29,10 @@ unlikely that you will need to customise it.
defmodule MyApp.Accounts.UserIdentity do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.UserIdentity]
extensions: [AshAuthentication.UserIdentity],
domain: MyApp.Accounts
user_identity do
api MyApp.Accounts
user_resource MyApp.Accounts.User
end
@ -60,7 +60,7 @@ Configure identity options for this resource
| 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. |
| [`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. |
| [`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. |

View file

@ -17,7 +17,8 @@ the `AshAuthentication` extension on your resource:
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshAuthentication]
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
@ -26,8 +27,6 @@ defmodule MyApp.Accounts.User do
end
authentication do
api MyApp.Accounts
strategies do
password :password do
identity_field :email
@ -101,7 +100,7 @@ Configure authentication for this resource
| 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`). |
| [`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. |
| [`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.
> 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.
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
inside the `strategies` (or `add_ons`) section of the `authentication` DSL.
2. A strategy struct, which stores information about the strategy as
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
the entity and the resource.
4. An optional verifier, which can be used to verify the DSL state of the
entity and the resource after compilation.
4. The `AshAuthentication.Strategy` protocol, which provides the glue needed
for everything to wire up and wrappers around the actions needed to run on
the resource.
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.
2. A strategy struct, which stores information about the strategy as
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
the entity and the resource.
4. An optional verifier, which can be used to verify the DSL state of the
entity and the resource after compilation.
5. The `AshAuthentication.Strategy` protocol, which provides the glue needed
for everything to wire up and wrappers around the actions needed to run on
the resource.
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!
## 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`:
```elixir
@ -36,7 +36,7 @@ defmodule OnlyMartiesAtTheParty do
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:
```elixir
@ -87,22 +87,22 @@ end
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:
- `name` is the name for which the helper function will be generated in
the DSL (ie `only_marty do #... end`).
- `describe` and `examples` are used when generating documentation.
- `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
this module.
- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark
provides a number of additional types over the default ones though, so check
out `Spark.OptionsHelpers` for more information.
- `name` is the name for which the helper function will be generated in
the DSL (ie `only_marty do #... end`).
- `describe` and `examples` are used when generating documentation.
- `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
this module.
- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark
provides a number of additional types over the default ones though, so check
out `Spark.OptionsHelpers` for more information.
> 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
> `style: :add_on` in the `use` statement.
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
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
that it have a `resource` field which will be set to the module of the resource
it's attached to during compilation.
@ -123,11 +123,11 @@ by adding it to the `extensions` section of your resource:
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication, OnlyMartiesAtTheParty]
use Ash.Resource,
extensions: [AshAuthentication, OnlyMartiesAtTheParty],
domain: MyApp.Accounts
authentication do
api MyApp.Accounts
strategies do
only_marty do
name_field :name
@ -145,25 +145,25 @@ end
## Implementing the `AshAuthentication.Strategy` protocol
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:
- "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
phase for each HTTP endpoint you wish to support with your strategy. In our
case we just want one sign in endpoint.
- "actions" - actions are exactly as they sound - Resource actions which can
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).
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
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
to refer these by a more generic name rather than via the user-specified one
(eg `:register` vs `:register_with_password`).
- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`)
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
`routes/1` callback is used to retrieve this information from the strategy.
- "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
phase for each HTTP endpoint you wish to support with your strategy. In our
case we just want one sign in endpoint.
- "actions" - actions are exactly as they sound - Resource actions which can
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).
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
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
to refer these by a more generic name rather than via the user-specified one
(eg `:register` vs `:register_with_password`).
- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`)
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
`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
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
```
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.
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.
```elixir
def name(strategy), do: strategy.name
@ -187,7 +187,7 @@ and action.
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
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
```
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
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
`{:ok, Ash.Resource.record}` or `{:error, any}` then we can just pass it
straight into `store_authentication_result/2` from
`AshAuthentication.Plug.Helpers`.
@ -223,11 +223,11 @@ straight into `store_authentication_result/2` from
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
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
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.
In this example we're assuming that there is a default `read` action present on
@ -246,22 +246,23 @@ the resource.
```elixir
alias AshAuthentication.Errors.AuthenticationFailed
require Ash.Query
import Ash.Expr
def action(strategy, :sign_in, params, options) do
name_field = strategy.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
|> Ash.Query.filter(ref(^name_field) == ^name)
|> Ash.Query.filter(expr(^ref(name_field) == ^name))
|> then(fn query ->
if strategy.case_sensitive? do
Ash.Query.filter(query, like(ref(^name_field), "Marty%"))
Ash.Query.filter(query, like(^ref(name_field), "Marty%"))
else
Ash.Query.filter(query, ilike(ref(^name_field), "Marty%"))
Ash.Query.filter(query, ilike(^ref(name_field), "Marty%"))
end
end)
|> api.read(options)
|> domain.read(options)
|> case do
{:ok, [user]} ->
{:ok, user}
@ -282,7 +283,7 @@ end
## Bonus round - transformers and verifiers
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.
At the very least it is good practice to call
@ -294,7 +295,7 @@ imported by `use AshAuthentication.Strategy.Custom` for this purpose.
### Transformers
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:
```elixir
@ -303,9 +304,9 @@ def transform(strategy, _dsl_state) do
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
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:
```elixir
@ -332,12 +333,12 @@ end
```
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
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
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
[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.
Next, you need to configure an application in [the Auth0
dashboard](https://manage.auth0.com/) using the following steps:
1. Click "Create Application".
2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so
keep that in mind.
3. Select "Regular Web Application" and click "Create".
4. Switch to the "Settings" tab.
5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll
need them soon.
6. In the "Allowed Callback URLs" section, add your callback URL. The
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`.
1. Click "Create Application".
2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so
keep that in mind.
3. Select "Regular Web Application" and click "Create".
4. Switch to the "Settings" tab.
5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll
need them soon.
6. In the "Allowed Callback URLs" section, add your callback URL. The
callback URL is generated from the following information:
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".
- 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
`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:
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication]
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do
strategies do
@ -92,29 +96,32 @@ end
The values for this configuration should be:
* `client_id` - the client ID copied from the Auth0 settings page.
* `redirect_uri` - the URL to the generated auth routes in your application
(eg `http://localhost:4000/auth`).
* `client_secret` the client secret copied from the Auth0 settings page.
* `base_url` - the "domain" value copied from the Auth0 settings page prefixed
with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`).
- `client_id` - the client ID copied from the Auth0 settings page.
- `redirect_uri` - the URL to the generated auth routes in your application
(eg `http://localhost:4000/auth`).
- `client_secret` the client secret copied from the Auth0 settings page.
- `base_url` - the "domain" value copied from the Auth0 settings page prefixed
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
returning users. The default name of the action is `register_with_` followed by
the strategy name. In our case that is `register_with_auth0`.
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 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
can use to populate your user attributes as needed.
- `oauth_tokens` contains the [`POST /oauth/token` response from
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.
- `user_info` contains the [`GET /userinfo` response from
Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you
can use to populate your user attributes as needed.
- `oauth_tokens` contains the [`POST /oauth/token` response from
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
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`
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
define register and sign-in actions on your "user" resource. You are welcome to
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 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
configuration is present.
@ -60,7 +60,7 @@ The `AshAuthentication.Strategy.Password` DSL allows you to override any of the
### `AshAuthentication.Strategy.OAuth2`
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
on your configuration, which the strategy will attempt to validate for common
misconfigurations.
@ -74,7 +74,7 @@ change to take place.
### `AshAuthentication.TokenResource`
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.
### `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
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
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.
## 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.
First, let's define our API:
First, let's define our domain:
```elixir
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
use Ash.Api
use Ash.Domain
resources do
resource MyApp.Accounts.User
@ -105,15 +105,15 @@ defmodule MyApp.Accounts do
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
# 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
if token generation is enabled for any resources in your application. Most of
Next, let's define our `Token` resource. This resource is needed
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
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
use AshPostgres.Repo, otp_app: :my_app
def installed_extensions do
["uuid-ossp", "citext"]
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
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication.TokenResource]
token do
api MyApp.Accounts
end
extensions: [AshAuthentication.TokenResource],
domain: MyApp.Accounts
postgres do
table "tokens"
@ -170,17 +167,16 @@ defmodule MyApp.Accounts.User do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication],
authorizers: [Ash.Policy.Authorizer]
authorizers: [Ash.Policy.Authorizer],
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true, private?: true
attribute :email, :ci_string, allow_nil?: false, public?: true
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
end
authentication do
api MyApp.Accounts
strategies do
password :password do
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)
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
provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro
an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication
provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro
which handles routing of requests to the correct providers, and defines
callbacks for successful and unsuccessful outcomes.
@ -290,7 +286,7 @@ based on the contents of the session store or `Authorization` header.
## Supervisor
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).
### Example
@ -314,9 +310,9 @@ end
## Token generation
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
`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!),
a two-arity anonymous function, or a module which implements the
`AshAuthentication.Secret` behaviour.

View file

@ -6,37 +6,41 @@ GitHub for authentication.
First you need to configure an application in your [GitHub developer
settings](https://github.com/settings/developers):
1. Click the "New OAuth App" button.
2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so
keep that in mind.
3. Set "Homepage URL" appropriately for your application and environment.
4. In the "Authorization callback URL" section, add your callback URL. The
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`.
1. Click the "New OAuth App" button.
2. Set your application name to something that identifies it. You will likely
need separate applications for development and production environments, so
keep that in mind.
3. Set "Homepage URL" appropriately for your application and environment.
4. In the "Authorization callback URL" section, add your callback URL. The
callback URL is generated from the following information:
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".
- 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
`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
set up):
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication]
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
authentication do
strategies do
@ -82,28 +86,31 @@ end
The values for this configuration should be:
* `client_id` - the client ID copied from the GitHub settings page.
* `redirect_uri` - the URL to the generated auth routes in your application
(eg `http://localhost:4000/auth`).
* `client_secret` the client secret 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
(eg `http://localhost:4000/auth`).
- `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
returning users. The default name of the action is `register_with_` followed by
the strategy name. In our case that is `register_with_github`.
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 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)
which you can use to populate your user attributes as needed.
- `oauth_tokens` contains the [`POST /login/oauth/access_token` response from
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
- `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)
which you can use to populate your user attributes as needed.
- `oauth_tokens` contains the [`POST /login/oauth/access_token` response from
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.
```elixir
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:
``` elixir
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication]
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do
...
@ -34,9 +36,12 @@ end
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.
``` elixir
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource, extensions: [AshAuthentication]
use Ash.Resource,
extensions: [AshAuthentication],
domain: MyApp.Accounts
# ...
actions do
create :register_with_google do

View file

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

View file

@ -21,7 +21,8 @@ defmodule AshAuthentication.AddOn.Confirmation do
```elixir
defmodule MyApp.Accounts.User do
use Ash.Resource,
extensions: [AshAuthentication]
extensions: [AshAuthentication],
domain: MyApp.Accounts
attributes do
uuid_primary_key :id
@ -29,8 +30,6 @@ defmodule AshAuthentication.AddOn.Confirmation do
end
authentication do
api MyApp.Accounts
add_ons do
confirmation :confirm do
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}
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, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
{: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)
|> api.update(opts)
|> domain.update(opts)
else
:error -> {:error, InvalidToken.exception(type: :confirmation)}
{:error, reason} -> {:error, reason}
@ -52,7 +52,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|> Map.new()
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} <-
TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
{:ok, _token_record} <-
@ -68,7 +68,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
extra_data: changes,
purpose: to_string(Strategy.name(strategy))
})
|> api.create(Keyword.merge(opts, upsert?: true)) do
|> domain.create(Keyword.merge(opts, upsert?: true)) do
:ok
else
{:error, reason} ->
@ -88,7 +88,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
@spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
def get_changes(strategy, jti, opts \\ []) do
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} <-
TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
{:ok, [token_record]} <-
@ -101,7 +101,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
})
|> Query.set_context(%{strategy: strategy})
|> Query.for_read(get_changes_action, %{"jti" => jti})
|> api.read(opts) do
|> domain.read(opts) do
changes =
strategy.monitor_fields
|> Stream.map(&to_string/1)

View file

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

View file

@ -53,15 +53,18 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
defp do_change(changeset, strategy) do
changeset
|> Changeset.before_action(fn changeset ->
changeset
|> not_confirm_action(strategy)
|> should_confirm_action_type(strategy)
|> monitored_field_changing(strategy)
|> changes_would_be_valid()
|> maybe_inhibit_updates(strategy)
|> maybe_perform_confirmation(strategy, changeset)
end)
|> Changeset.before_action(
fn changeset ->
changeset
|> not_confirm_action(strategy)
|> should_confirm_action_type(strategy)
|> monitored_field_changing(strategy)
|> changes_would_be_valid()
|> maybe_inhibit_updates(strategy)
|> maybe_perform_confirmation(strategy, changeset)
end,
prepend?: true
)
end
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),
{:ok, attribute} <- find_attribute(dsl_state, field),
: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
{:cont, :ok}
else
@ -142,7 +143,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
accept: strategy.monitor_fields,
arguments: arguments,
metadata: metadata,
changes: changes
changes: changes,
require_atomic?: false
)
end
@ -150,8 +152,26 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
with {:ok, action} <- validate_action_exists(dsl_state, strategy.confirm_action_name),
:ok <- validate_action_has_change(action, Confirmation.ConfirmChange),
:ok <- validate_action_argument_option(action, :confirm, :allow_nil?, [false]),
:ok <- validate_action_has_change(action, GenerateTokenChange) do
validate_action_argument_option(action, :confirm, :type, [Type.String])
:ok <- 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

View file

@ -8,7 +8,7 @@ defmodule AshAuthentication.Dsl do
import AshAuthentication.Utils, only: [to_sentence: 2]
import Joken.Signer, only: [algorithms: 0]
alias Ash.{Api, Resource}
alias Ash.{Domain, Resource}
@default_token_lifetime_days 14
@ -41,17 +41,18 @@ defmodule AshAuthentication.Dsl do
%Section{
name: :authentication,
describe: "Configure authentication for this resource",
modules: [:api],
modules: [:domain],
schema: [
subject_name: [
type: :atom,
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`)."
],
api: [
type: {:behaviour, Api},
domain: [
type: {:behaviour, Domain},
required: false,
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: [
type: :atom,

View file

@ -3,16 +3,20 @@ defmodule AshAuthentication.Errors.AuthenticationFailed do
A generic, authentication failed error.
"""
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()
def exception(args) do
args
|> super()
|> describe()
end
def message(_), do: "Authentication failed"
defimpl Ash.ErrorKind do
@moduledoc false

View file

@ -3,7 +3,9 @@ defmodule AshAuthentication.Errors.InvalidToken do
An invalid token was presented.
"""
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
@moduledoc false

View file

@ -3,7 +3,11 @@ defmodule AshAuthentication.Errors.MissingSecret do
A secret is now missing.
"""
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
@moduledoc false

View file

@ -7,7 +7,7 @@ defmodule AshAuthentication.Info do
extension: AshAuthentication,
sections: [:authentication]
alias Ash.{Changeset, Query}
alias Ash.{Changeset, Domain, Query, Resource}
alias AshAuthentication.Strategy
alias Spark.Dsl.Extension
@ -103,4 +103,33 @@ defmodule AshAuthentication.Info do
{:ok, strategy}
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

View file

@ -3,7 +3,7 @@ defmodule AshAuthentication.Plug.Macros do
Generators used within `use AshAuthentication.Plug`.
"""
alias Ash.Api
alias Ash.Domain
alias AshAuthentication.Plug.Helpers
alias Plug.Conn
alias Spark.Dsl.Extension
@ -14,11 +14,9 @@ defmodule AshAuthentication.Plug.Macros do
@spec validate_subject_name_uniqueness(atom) :: Macro.t()
defmacro validate_subject_name_uniqueness(otp_app) do
quote do
require Ash.Api.Info
unquote(otp_app)
|> Application.compile_env(:ash_apis, [])
|> Stream.flat_map(&Api.Info.depend_on_resources(&1))
|> Application.compile_env(:ash_domains, [])
|> Stream.flat_map(&Domain.Info.resources(&1))
|> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)})
|> Stream.reject(&(elem(&1, 1) == nil))
|> 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__)
quote do
require Ash.Api.Info
use Plug.Router
plug(:match)
plug(:dispatch)
routes =
unquote(otp_app)
|> Application.compile_env(:ash_apis, [])
|> Stream.flat_map(&Ash.Api.Info.depend_on_resources(&1))
|> Application.compile_env(:ash_domains, [])
|> Stream.flat_map(&Ash.Domain.Info.resources(&1))
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|> Stream.flat_map(&Info.authentication_strategies/1)
|> Stream.flat_map(fn strategy ->

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
@doc false
@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
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)
query
|> Query.filter(ref(^identity_field) == ^identity)
|> Query.filter(^ref(identity_field) == ^identity)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders)
end)

View file

@ -10,7 +10,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
@doc false
@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
subject_name =
query.resource

View file

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

View file

@ -22,7 +22,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
)}
def sign_in(%OAuth2{} = strategy, params, options) do
api = Info.authentication_api!(strategy.resource)
domain = Info.domain!(strategy.resource)
strategy.resource
|> Query.new()
@ -32,7 +32,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
}
})
|> Query.for_read(strategy.sign_in_action_name, params)
|> api.read(options)
|> domain.read(options)
|> case do
{: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}
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)
strategy.resource
@ -104,7 +104,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
upsert?: true,
upsert_identity: action.upsert_identity
)
|> api.create(options)
|> domain.create(options)
end
def register(%OAuth2{} = strategy, _params, _options),

View file

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

View file

@ -17,7 +17,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
@doc false
@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
case Info.strategy_for_action(query.resource, query.action.name) do
:error ->
@ -70,7 +70,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|> case do
{:ok, _identity} ->
user
|> query.api.load(strategy.identity_relationship_name)
|> query.domain.load(strategy.identity_relationship_name)
{:error, reason} ->
{:error, reason}

View file

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

View file

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

View file

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

View file

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

View file

@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
@doc false
@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
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)
query
|> Query.filter(ref(^identity_field) == ^identity)
|> Query.filter(^ref(identity_field) == ^identity)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, select_for_senders)
end)

View file

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

View file

@ -19,14 +19,14 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
@doc false
@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
{:ok, strategy} = Info.find_strategy(query, context, options)
identity_field = strategy.identity_field
identity = Query.get_argument(query, identity_field)
query
|> Query.filter(ref(^identity_field) == ^identity)
|> Query.filter(^ref(identity_field) == ^identity)
|> check_sign_in_token_configuration(strategy)
|> Query.before_action(fn query ->
Ash.Query.ensure_selected(query, [strategy.hashed_password_field])

View file

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

View file

@ -120,7 +120,7 @@ defprotocol AshAuthentication.Strategy do
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) ::
:ok | {:ok, Resource.record()} | {:error, any}

View file

@ -41,7 +41,7 @@ defmodule AshAuthentication.Supervisor do
raise """
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:
Suggestion, try adding `{AshAuthentication.Supervisor, otp_app: :my_app}`

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetConfirmationChangesPreparation do
@doc false
@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
jti = Query.get_argument(query, :jti)
strategy = query.context.strategy

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetTokenPreparation do
@doc false
@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
jti = get_jti(query)
purpose = Query.get_argument(query, :purpose)

View file

@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.IsRevokedPreparation do
@doc false
@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
case get_jti(query) do
{:ok, jti} ->

View file

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

View file

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

View file

@ -30,7 +30,7 @@ defmodule AshAuthentication.Transformer do
@spec transform(map) ::
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
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_unique_strategy_names(dsl_state),
:ok <- validate_unique_add_on_names(dsl_state),

View file

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

View file

@ -3,7 +3,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
Code interface for provider identity actions.
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`.
"""
@ -15,7 +15,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
"""
@spec upsert(Resource.t(), map) :: {:ok, Resource.record()} | {:error, term}
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} <-
UserIdentity.Info.user_identity_upsert_action_name(resource),
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_identity: action.upsert_identity
)
|> api.create()
|> domain.create()
end
end
end

View file

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

View file

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

View file

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

View file

@ -19,6 +19,7 @@ defmodule AshAuthentication.Validations.Attribute do
:error ->
{:error,
DslError.exception(
module: resource,
path: [:actions, :attribute],
message:
"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,
DslError.exception(
module: resource,
path: [:actions, :attribute],
message:
"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] ->
{:error,
DslError.exception(
module: resource,
path: [:actions, :attribute],
message:
"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,
DslError.exception(
module: resource,
path: [:actions, :attribute],
message:
"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}
| {:warn, String.t() | list(String.t())}
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)
end
end
defp validate_api_presence(dsl_state) do
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:authentication], :api),
:ok <- assert_is_module(api),
true <- function_exported?(api, :spark_is, 0),
Ash.Api <- api.spark_is() do
{:ok, api}
defp validate_domain_presence(dsl_state) do
with domain when not is_nil(domain) <-
Transformer.get_option(dsl_state, [:authentication], :domain),
:ok <- assert_is_module(domain),
true <- function_exported?(domain, :spark_is, 0),
Ash.Domain <- domain.spark_is() do
{:ok, domain}
else
nil ->
{:error,
DslError.exception(
path: [:authentication, :api],
message: "An API module must be present"
path: [:authentication, :domain],
message: "A domain module must be present"
)}
_ ->
{:error,
DslError.exception(
path: [:authentication, :api],
message: "Module is not an Ash.Api."
path: [:authentication, :domain],
message: "Module is not an `Ash.Domain`."
)}
end
end

13
mix.exs
View file

@ -173,7 +173,7 @@ defmodule AshAuthentication.MixProject do
# Run "mix help deps" to learn about dependencies.
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"},
{:bcrypt_elixir, "~> 3.0"},
{:castore, "~> 1.0"},
@ -181,16 +181,17 @@ defmodule AshAuthentication.MixProject do
{:jason, "~> 1.4"},
{:joken, "~> 2.5"},
{:plug, "~> 1.13"},
{:spark, "~> 1.1 and >= 1.1.39"},
{:spark, "~> 2.0"},
{:splode, "~> 0.2"},
{:absinthe_plug, "~> 1.5", only: [:dev, :test]},
{:ash_graphql, "~> 0.21", only: [:dev, :test]},
{:ash_json_api, "~> 0.30", only: [:dev, :test]},
{:ash_postgres, "~> 1.5.1", optional: true},
# {:ash_graphql, "~> 0.21", only: [:dev, :test]},
# {:ash_json_api, "~> 0.30", only: [:dev, :test]},
{:ash_postgres, "== 2.0.0-rc.1", optional: true},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
{:doctor, "~> 0.18", 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]},
{:git_ops, "~> 2.4", only: [:dev, :test], runtime: false},
{: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"},
"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_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_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"},
"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_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"},
"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"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
"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"},
"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_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"},
@ -20,7 +17,6 @@
"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"},
"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"},
"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"},
@ -28,7 +24,7 @@
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
"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"},
"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"},
@ -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"},
"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"},
"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"},
"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_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_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
"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.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.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"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"},
"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_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"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_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"},
"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"},
"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"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
"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"},
"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
{:ok, strategy} = Info.strategy(Example.User, :confirm)
user = build_user()
new_username = username()
@ -45,11 +46,16 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, [])
assert confirmed_user.id == user.id
assert to_string(confirmed_user.username) == new_username
assert_in_delta DateTime.to_unix(confirmed_user.confirmed_at),
DateTime.to_unix(DateTime.utc_now()),
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

View file

@ -122,33 +122,6 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
assert claims["sub"] =~ "user?id=#{user.id}"
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
user = build_user()
{:ok, strategy} = Info.strategy(Example.User, :password)
@ -208,7 +181,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
capture_log(fn ->
params = %{"username" => user.username}
options = []
api = Info.authentication_api!(strategy.resource)
domain = Info.domain!(strategy.resource)
resettable = strategy.resettable
result =
@ -221,7 +194,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
})
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|> Ash.Query.select([])
|> api.read(options)
|> domain.read(options)
|> case do
{:ok, _} -> :ok
{:error, reason} -> {:error, reason}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
defmodule Example do
@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
resource Example.User
@ -9,7 +10,7 @@ defmodule Example do
resource Example.UserIdentity
end
json_api do
prefix "/api"
end
# json_api do
# prefix "/api"
# end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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