From f0075e2cd32c5f736fdd78890c2f4efe239d94f4 Mon Sep 17 00:00:00 2001 From: James Harton Date: Thu, 28 Mar 2024 14:39:53 +1300 Subject: [PATCH] improvement!: Update to support Ash 3.0. (#599) --- .formatter.exs | 5 +- config/dev.exs | 2 +- config/test.exs | 2 +- dev/dev_server/api_router.ex | 24 ++-- dev/dev_server/gql_router.ex | 46 +++--- dev/dev_server/json_api_router.ex | 8 +- dev/dev_server/router.ex | 4 +- dev/dev_server/test_page.html.eex | 2 +- ...L:-AshAuthentication.AddOn.Confirmation.md | 5 +- ...L:-AshAuthentication.Strategy.MagicLink.md | 5 +- .../DSL:-AshAuthentication.Strategy.OAuth2.md | 9 +- ...SL:-AshAuthentication.Strategy.Password.md | 5 +- .../DSL:-AshAuthentication.TokenResource.md | 9 +- .../DSL:-AshAuthentication.UserIdentity.md | 6 +- documentation/dsls/DSL:-AshAuthentication.md | 7 +- documentation/topics/custom-strategy.md | 131 +++++++++--------- documentation/tutorials/auth0-quickstart.md | 87 ++++++------ .../getting-started-with-authentication.md | 52 ++++--- documentation/tutorials/github-quickstart.md | 83 ++++++----- documentation/tutorials/google-quickstart.md | 13 +- lib/ash_authentication.ex | 19 ++- .../add_ons/confirmation.ex | 5 +- .../add_ons/confirmation/actions.ex | 12 +- .../add_ons/confirmation/confirm_change.ex | 12 +- .../confirmation/confirmation_hook_change.ex | 21 +-- .../add_ons/confirmation/transformer.ex | 26 +++- lib/ash_authentication/dsl.ex | 11 +- .../errors/authentication_failed.ex | 18 ++- .../errors/invalid_token.ex | 4 +- .../errors/missing_secret.ex | 6 +- lib/ash_authentication/info.ex | 31 ++++- lib/ash_authentication/plug/macros.ex | 8 +- lib/ash_authentication/plug/router.ex | 5 +- lib/ash_authentication/secret.ex | 12 +- lib/ash_authentication/sender.ex | 14 +- .../strategies/magic_link.ex | 5 +- .../strategies/magic_link/actions.ex | 8 +- .../magic_link/request_preparation.ex | 4 +- .../magic_link/sign_in_preparation.ex | 2 +- lib/ash_authentication/strategies/oauth2.ex | 9 +- .../strategies/oauth2/actions.ex | 8 +- .../strategies/oauth2/identity_change.ex | 2 +- .../strategies/oauth2/sign_in_preparation.ex | 4 +- lib/ash_authentication/strategies/password.ex | 5 +- .../strategies/password/actions.ex | 20 +-- .../password_confirmation_validation.ex | 14 +- .../password/password_validation.ex | 6 +- .../request_password_reset_preparation.ex | 4 +- .../password/reset_token_validation.ex | 6 +- .../password/sign_in_preparation.ex | 4 +- .../sign_in_with_token_preparation.ex | 2 +- .../strategies/password/transformer.ex | 11 +- lib/ash_authentication/strategy.ex | 2 +- lib/ash_authentication/supervisor.ex | 2 +- lib/ash_authentication/token_resource.ex | 16 +-- .../token_resource/actions.ex | 62 +++------ .../token_resource/expunger.ex | 4 +- .../get_confirmation_changes_preparation.ex | 2 +- .../token_resource/get_token_preparation.ex | 2 +- .../token_resource/is_revoked_preparation.ex | 2 +- .../token_resource/transformer.ex | 36 +++-- .../token_resource/verifier.ex | 22 +-- lib/ash_authentication/transformer.ex | 2 +- lib/ash_authentication/user_identity.ex | 13 +- .../user_identity/actions.ex | 6 +- .../user_identity/transformer.ex | 16 ++- .../user_identity/verifier.ex | 8 +- lib/ash_authentication/utils.ex | 22 +-- lib/ash_authentication/validations/action.ex | 48 +++++++ .../validations/attribute.ex | 4 + lib/ash_authentication/verifier.ex | 23 +-- mix.exs | 13 +- mix.lock | 29 ++-- ...0240328001848_install_2_extensions_wat.exs | 21 +++ ...240328002131_update_timestamp_defaults.exs | 55 ++++++++ .../{ => repo}/extensions.json | 0 .../repo/tokens/20240328002131.json | 89 ++++++++++++ .../repo/user/20240328002131.json | 109 +++++++++++++++ .../repo/user_identities/20240328002131.json | 119 ++++++++++++++++ .../20240328002131.json | 79 +++++++++++ .../add_ons/confirmation/actions_test.exs | 8 +- .../strategies/password/actions_test.exs | 31 +---- .../password/hash_password_change_test.exs | 8 +- .../password_confirmation_validation_test.exs | 8 +- .../password/password_validation_test.exs | 18 ++- test/support/data_case.ex | 4 +- test/support/example.ex | 9 +- .../example/only_marties_at_the_party.ex | 11 +- test/support/example/schema.ex | 16 +-- test/support/example/token.ex | 7 +- test/support/example/user.ex | 60 ++++---- test/support/example/user_identity.ex | 4 +- .../example/user_with_token_required.ex | 12 +- 93 files changed, 1200 insertions(+), 625 deletions(-) create mode 100644 priv/repo/migrations/20240328001848_install_2_extensions_wat.exs create mode 100644 priv/repo/migrations/20240328002131_update_timestamp_defaults.exs rename priv/resource_snapshots/{ => repo}/extensions.json (100%) create mode 100644 priv/resource_snapshots/repo/tokens/20240328002131.json create mode 100644 priv/resource_snapshots/repo/user/20240328002131.json create mode 100644 priv/resource_snapshots/repo/user_identities/20240328002131.json create mode 100644 priv/resource_snapshots/repo/user_with_token_required/20240328002131.json diff --git a/.formatter.exs b/.formatter.exs index 53d5a19..e11fe37 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -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}" diff --git a/config/dev.exs b/config/dev.exs index 1140312..ce0132d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -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", diff --git a/config/test.exs b/config/test.exs index 15156b5..b19f81d 100644 --- a/config/test.exs +++ b/config/test.exs @@ -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", diff --git a/dev/dev_server/api_router.ex b/dev/dev_server/api_router.ex index eb2da81..786f81d 100644 --- a/dev/dev_server/api_router.ex +++ b/dev/dev_server/api_router.ex @@ -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 diff --git a/dev/dev_server/gql_router.ex b/dev/dev_server/gql_router.ex index 1fe3c4a..b3c29c9 100644 --- a/dev/dev_server/gql_router.ex +++ b/dev/dev_server/gql_router.ex @@ -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 diff --git a/dev/dev_server/json_api_router.ex b/dev/dev_server/json_api_router.ex index 6ab03b9..5ac39b6 100644 --- a/dev/dev_server/json_api_router.ex +++ b/dev/dev_server/json_api_router.ex @@ -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 diff --git a/dev/dev_server/router.ex b/dev/dev_server/router.ex index 2a43c19..4bea6a0 100644 --- a/dev/dev_server/router.ex +++ b/dev/dev_server/router.ex @@ -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 diff --git a/dev/dev_server/test_page.html.eex b/dev/dev_server/test_page.html.eex index fd3e2ed..0d25f8c 100644 --- a/dev/dev_server/test_page.html.eex +++ b/dev/dev_server/test_page.html.eex @@ -10,7 +10,7 @@

Resources:

<%= for {resource, options, strategies} <- @resources do %> -

<%= inspect(options.subject_name) %> - <%= Ash.Api.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %>

+

<%= inspect(options.subject_name) %> - <%= Ash.Domain.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %>

<%= for strategy <- strategies do %> diff --git a/documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md b/documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md index 9f8e263..faf477a 100644 --- a/documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md +++ b/documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md @@ -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] diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md index a8f90bc..e68578e 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md @@ -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 diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md index 0e3a7d1..44596ae 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md @@ -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 diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md index 0f0c394..cdda8f7 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md @@ -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 diff --git a/documentation/dsls/DSL:-AshAuthentication.TokenResource.md b/documentation/dsls/DSL:-AshAuthentication.TokenResource.md index 412092c..6860a97 100644 --- a/documentation/dsls/DSL:-AshAuthentication.TokenResource.md +++ b/documentation/dsls/DSL:-AshAuthentication.TokenResource.md @@ -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. | diff --git a/documentation/dsls/DSL:-AshAuthentication.UserIdentity.md b/documentation/dsls/DSL:-AshAuthentication.UserIdentity.md index b4b16fc..18ba234 100644 --- a/documentation/dsls/DSL:-AshAuthentication.UserIdentity.md +++ b/documentation/dsls/DSL:-AshAuthentication.UserIdentity.md @@ -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. | diff --git a/documentation/dsls/DSL:-AshAuthentication.md b/documentation/dsls/DSL:-AshAuthentication.md index f5b6c2c..2b7e122 100644 --- a/documentation/dsls/DSL:-AshAuthentication.md +++ b/documentation/dsls/DSL:-AshAuthentication.md @@ -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. | diff --git a/documentation/topics/custom-strategy.md b/documentation/topics/custom-strategy.md index 550709e..8d7c74f 100644 --- a/documentation/topics/custom-strategy.md +++ b/documentation/topics/custom-strategy.md @@ -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. diff --git a/documentation/tutorials/auth0-quickstart.md b/documentation/tutorials/auth0-quickstart.md index 7eeba1d..f66db5c 100644 --- a/documentation/tutorials/auth0-quickstart.md +++ b/documentation/tutorials/auth0-quickstart.md @@ -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 # ... diff --git a/documentation/tutorials/getting-started-with-authentication.md b/documentation/tutorials/getting-started-with-authentication.md index 038b2fc..b52e2f2 100644 --- a/documentation/tutorials/getting-started-with-authentication.md +++ b/documentation/tutorials/getting-started-with-authentication.md @@ -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. diff --git a/documentation/tutorials/github-quickstart.md b/documentation/tutorials/github-quickstart.md index 6da0498..6edd913 100644 --- a/documentation/tutorials/github-quickstart.md +++ b/documentation/tutorials/github-quickstart.md @@ -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 # ... diff --git a/documentation/tutorials/google-quickstart.md b/documentation/tutorials/google-quickstart.md index 95e9353..879bd80 100644 --- a/documentation/tutorials/google-quickstart.md +++ b/documentation/tutorials/google-quickstart.md @@ -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 diff --git a/lib/ash_authentication.ex b/lib/ash_authentication.ex index 5cf5324..511ddf3 100644 --- a/lib/ash_authentication.ex +++ b/lib/ash_authentication.ex @@ -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([])} diff --git a/lib/ash_authentication/add_ons/confirmation.ex b/lib/ash_authentication/add_ons/confirmation.ex index d341cf6..5a05dd3 100644 --- a/lib/ash_authentication/add_ons/confirmation.ex +++ b/lib/ash_authentication/add_ons/confirmation.ex @@ -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] diff --git a/lib/ash_authentication/add_ons/confirmation/actions.ex b/lib/ash_authentication/add_ons/confirmation/actions.ex index 1a58a75..8df33df 100644 --- a/lib/ash_authentication/add_ons/confirmation/actions.ex +++ b/lib/ash_authentication/add_ons/confirmation/actions.ex @@ -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) diff --git a/lib/ash_authentication/add_ons/confirmation/confirm_change.ex b/lib/ash_authentication/add_ons/confirmation/confirm_change.ex index 1e2a0ce..5850f32 100644 --- a/lib/ash_authentication/add_ons/confirmation/confirm_change.ex +++ b/lib/ash_authentication/add_ons/confirmation/confirm_change.ex @@ -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 diff --git a/lib/ash_authentication/add_ons/confirmation/confirmation_hook_change.ex b/lib/ash_authentication/add_ons/confirmation/confirmation_hook_change.ex index 9406d65..4dbbc2b 100644 --- a/lib/ash_authentication/add_ons/confirmation/confirmation_hook_change.ex +++ b/lib/ash_authentication/add_ons/confirmation/confirmation_hook_change.ex @@ -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) diff --git a/lib/ash_authentication/add_ons/confirmation/transformer.ex b/lib/ash_authentication/add_ons/confirmation/transformer.ex index 0885721..94dd5db 100644 --- a/lib/ash_authentication/add_ons/confirmation/transformer.ex +++ b/lib/ash_authentication/add_ons/confirmation/transformer.ex @@ -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 diff --git a/lib/ash_authentication/dsl.ex b/lib/ash_authentication/dsl.ex index 14b0df9..8a63614 100644 --- a/lib/ash_authentication/dsl.ex +++ b/lib/ash_authentication/dsl.ex @@ -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, diff --git a/lib/ash_authentication/errors/authentication_failed.ex b/lib/ash_authentication/errors/authentication_failed.ex index f5d2722..2aee39d 100644 --- a/lib/ash_authentication/errors/authentication_failed.ex +++ b/lib/ash_authentication/errors/authentication_failed.ex @@ -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 diff --git a/lib/ash_authentication/errors/invalid_token.ex b/lib/ash_authentication/errors/invalid_token.ex index d953463..30ff67b 100644 --- a/lib/ash_authentication/errors/invalid_token.ex +++ b/lib/ash_authentication/errors/invalid_token.ex @@ -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 diff --git a/lib/ash_authentication/errors/missing_secret.ex b/lib/ash_authentication/errors/missing_secret.ex index 19bc30f..435c80b 100644 --- a/lib/ash_authentication/errors/missing_secret.ex +++ b/lib/ash_authentication/errors/missing_secret.ex @@ -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 diff --git a/lib/ash_authentication/info.ex b/lib/ash_authentication/info.ex index c50db52..93d8c94 100644 --- a/lib/ash_authentication/info.ex +++ b/lib/ash_authentication/info.ex @@ -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 diff --git a/lib/ash_authentication/plug/macros.ex b/lib/ash_authentication/plug/macros.ex index 10377a9..01013e3 100644 --- a/lib/ash_authentication/plug/macros.ex +++ b/lib/ash_authentication/plug/macros.ex @@ -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}) diff --git a/lib/ash_authentication/plug/router.ex b/lib/ash_authentication/plug/router.ex index 33bdcb9..b0d20c2 100644 --- a/lib/ash_authentication/plug/router.ex +++ b/lib/ash_authentication/plug/router.ex @@ -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 -> diff --git a/lib/ash_authentication/secret.ex b/lib/ash_authentication/secret.ex index abee2bc..61d3eac 100644 --- a/lib/ash_authentication/secret.ex +++ b/lib/ash_authentication/secret.ex @@ -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 -> diff --git a/lib/ash_authentication/sender.ex b/lib/ash_authentication/sender.ex index 031b4b6..d786f08 100644 --- a/lib/ash_authentication/sender.ex +++ b/lib/ash_authentication/sender.ex @@ -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 diff --git a/lib/ash_authentication/strategies/magic_link.ex b/lib/ash_authentication/strategies/magic_link.ex index 2af8830..f1234fa 100644 --- a/lib/ash_authentication/strategies/magic_link.ex +++ b/lib/ash_authentication/strategies/magic_link.ex @@ -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 diff --git a/lib/ash_authentication/strategies/magic_link/actions.ex b/lib/ash_authentication/strategies/magic_link/actions.ex index b21d9f2..186f379 100644 --- a/lib/ash_authentication/strategies/magic_link/actions.ex +++ b/lib/ash_authentication/strategies/magic_link/actions.ex @@ -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} diff --git a/lib/ash_authentication/strategies/magic_link/request_preparation.ex b/lib/ash_authentication/strategies/magic_link/request_preparation.ex index 400a99f..61c6167 100644 --- a/lib/ash_authentication/strategies/magic_link/request_preparation.ex +++ b/lib/ash_authentication/strategies/magic_link/request_preparation.ex @@ -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) diff --git a/lib/ash_authentication/strategies/magic_link/sign_in_preparation.ex b/lib/ash_authentication/strategies/magic_link/sign_in_preparation.ex index e0f30a7..b9cfa38 100644 --- a/lib/ash_authentication/strategies/magic_link/sign_in_preparation.ex +++ b/lib/ash_authentication/strategies/magic_link/sign_in_preparation.ex @@ -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 diff --git a/lib/ash_authentication/strategies/oauth2.ex b/lib/ash_authentication/strategies/oauth2.ex index 2f8ebe6..80deda9 100644 --- a/lib/ash_authentication/strategies/oauth2.ex +++ b/lib/ash_authentication/strategies/oauth2.ex @@ -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 diff --git a/lib/ash_authentication/strategies/oauth2/actions.ex b/lib/ash_authentication/strategies/oauth2/actions.ex index 8d3b8d2..2a99926 100644 --- a/lib/ash_authentication/strategies/oauth2/actions.ex +++ b/lib/ash_authentication/strategies/oauth2/actions.ex @@ -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), diff --git a/lib/ash_authentication/strategies/oauth2/identity_change.ex b/lib/ash_authentication/strategies/oauth2/identity_change.ex index ea55ba0..b62c828 100644 --- a/lib/ash_authentication/strategies/oauth2/identity_change.ex +++ b/lib/ash_authentication/strategies/oauth2/identity_change.ex @@ -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} diff --git a/lib/ash_authentication/strategies/oauth2/sign_in_preparation.ex b/lib/ash_authentication/strategies/oauth2/sign_in_preparation.ex index ea1a01d..b369690 100644 --- a/lib/ash_authentication/strategies/oauth2/sign_in_preparation.ex +++ b/lib/ash_authentication/strategies/oauth2/sign_in_preparation.ex @@ -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} diff --git a/lib/ash_authentication/strategies/password.ex b/lib/ash_authentication/strategies/password.ex index f545a9a..ea9cb1e 100644 --- a/lib/ash_authentication/strategies/password.ex +++ b/lib/ash_authentication/strategies/password.ex @@ -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 diff --git a/lib/ash_authentication/strategies/password/actions.ex b/lib/ash_authentication/strategies/password/actions.ex index adabf5c..e70b847 100644 --- a/lib/ash_authentication/strategies/password/actions.ex +++ b/lib/ash_authentication/strategies/password/actions.ex @@ -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)} diff --git a/lib/ash_authentication/strategies/password/password_confirmation_validation.ex b/lib/ash_authentication/strategies/password/password_confirmation_validation.ex index 052cf10..62f8f14 100644 --- a/lib/ash_authentication/strategies/password/password_confirmation_validation.ex +++ b/lib/ash_authentication/strategies/password/password_confirmation_validation.ex @@ -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) diff --git a/lib/ash_authentication/strategies/password/password_validation.ex b/lib/ash_authentication/strategies/password/password_validation.ex index 4aac19e..d3df61e 100644 --- a/lib/ash_authentication/strategies/password/password_validation.ex +++ b/lib/ash_authentication/strategies/password/password_validation.ex @@ -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), diff --git a/lib/ash_authentication/strategies/password/request_password_reset_preparation.ex b/lib/ash_authentication/strategies/password/request_password_reset_preparation.ex index b3cbc45..14b1068 100644 --- a/lib/ash_authentication/strategies/password/request_password_reset_preparation.ex +++ b/lib/ash_authentication/strategies/password/request_password_reset_preparation.ex @@ -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) diff --git a/lib/ash_authentication/strategies/password/reset_token_validation.ex b/lib/ash_authentication/strategies/password/reset_token_validation.ex index cd2f2ef..7f014bd 100644 --- a/lib/ash_authentication/strategies/password/reset_token_validation.ex +++ b/lib/ash_authentication/strategies/password/reset_token_validation.ex @@ -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), diff --git a/lib/ash_authentication/strategies/password/sign_in_preparation.ex b/lib/ash_authentication/strategies/password/sign_in_preparation.ex index 635abaf..51cefb5 100644 --- a/lib/ash_authentication/strategies/password/sign_in_preparation.ex +++ b/lib/ash_authentication/strategies/password/sign_in_preparation.ex @@ -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]) diff --git a/lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex b/lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex index e769e95..2bc1a58 100644 --- a/lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex +++ b/lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex @@ -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) diff --git a/lib/ash_authentication/strategies/password/transformer.ex b/lib/ash_authentication/strategies/password/transformer.ex index de01044..f0f6c52 100644 --- a/lib/ash_authentication/strategies/password/transformer.ex +++ b/lib/ash_authentication/strategies/password/transformer.ex @@ -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 diff --git a/lib/ash_authentication/strategy.ex b/lib/ash_authentication/strategy.ex index cbefea6..23e0029 100644 --- a/lib/ash_authentication/strategy.ex +++ b/lib/ash_authentication/strategy.ex @@ -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} diff --git a/lib/ash_authentication/supervisor.ex b/lib/ash_authentication/supervisor.ex index fa3921a..d90fc36 100644 --- a/lib/ash_authentication/supervisor.ex +++ b/lib/ash_authentication/supervisor.ex @@ -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}` diff --git a/lib/ash_authentication/token_resource.ex b/lib/ash_authentication/token_resource.ex index 7434a27..c38ddaf 100644 --- a/lib/ash_authentication/token_resource.ex +++ b/lib/ash_authentication/token_resource.ex @@ -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" diff --git a/lib/ash_authentication/token_resource/actions.ex b/lib/ash_authentication/token_resource/actions.ex index 3f5496b..d79f173 100644 --- a/lib/ash_authentication/token_resource/actions.ex +++ b/lib/ash_authentication/token_resource/actions.ex @@ -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 diff --git a/lib/ash_authentication/token_resource/expunger.ex b/lib/ash_authentication/token_resource/expunger.ex index b05e349..1824a87 100644 --- a/lib/ash_authentication/token_resource/expunger.ex +++ b/lib/ash_authentication/token_resource/expunger.ex @@ -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 diff --git a/lib/ash_authentication/token_resource/get_confirmation_changes_preparation.ex b/lib/ash_authentication/token_resource/get_confirmation_changes_preparation.ex index d801451..23dda97 100644 --- a/lib/ash_authentication/token_resource/get_confirmation_changes_preparation.ex +++ b/lib/ash_authentication/token_resource/get_confirmation_changes_preparation.ex @@ -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 diff --git a/lib/ash_authentication/token_resource/get_token_preparation.ex b/lib/ash_authentication/token_resource/get_token_preparation.ex index 6703ee4..7cbb6cc 100644 --- a/lib/ash_authentication/token_resource/get_token_preparation.ex +++ b/lib/ash_authentication/token_resource/get_token_preparation.ex @@ -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) diff --git a/lib/ash_authentication/token_resource/is_revoked_preparation.ex b/lib/ash_authentication/token_resource/is_revoked_preparation.ex index 6e5a5d9..bb24fb1 100644 --- a/lib/ash_authentication/token_resource/is_revoked_preparation.ex +++ b/lib/ash_authentication/token_resource/is_revoked_preparation.ex @@ -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} -> diff --git a/lib/ash_authentication/token_resource/transformer.ex b/lib/ash_authentication/token_resource/transformer.ex index c17be3c..b4bc019 100644 --- a/lib/ash_authentication/token_resource/transformer.ex +++ b/lib/ash_authentication/token_resource/transformer.ex @@ -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 diff --git a/lib/ash_authentication/token_resource/verifier.ex b/lib/ash_authentication/token_resource/verifier.ex index 8ee964b..4920789 100644 --- a/lib/ash_authentication/token_resource/verifier.ex +++ b/lib/ash_authentication/token_resource/verifier.ex @@ -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 diff --git a/lib/ash_authentication/transformer.ex b/lib/ash_authentication/transformer.ex index a253424..79c5bdf 100644 --- a/lib/ash_authentication/transformer.ex +++ b/lib/ash_authentication/transformer.ex @@ -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), diff --git a/lib/ash_authentication/user_identity.ex b/lib/ash_authentication/user_identity.ex index 785cc47..99bf222 100644 --- a/lib/ash_authentication/user_identity.ex +++ b/lib/ash_authentication/user_identity.ex @@ -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 diff --git a/lib/ash_authentication/user_identity/actions.ex b/lib/ash_authentication/user_identity/actions.ex index d91af12..501f807 100644 --- a/lib/ash_authentication/user_identity/actions.ex +++ b/lib/ash_authentication/user_identity/actions.ex @@ -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 diff --git a/lib/ash_authentication/user_identity/transformer.ex b/lib/ash_authentication/user_identity/transformer.ex index c8c0c23..171371a 100644 --- a/lib/ash_authentication/user_identity/transformer.ex +++ b/lib/ash_authentication/user_identity/transformer.ex @@ -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 diff --git a/lib/ash_authentication/user_identity/verifier.ex b/lib/ash_authentication/user_identity/verifier.ex index 808e34c..1633c47 100644 --- a/lib/ash_authentication/user_identity/verifier.ex +++ b/lib/ash_authentication/user_identity/verifier.ex @@ -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 diff --git a/lib/ash_authentication/utils.ex b/lib/ash_authentication/utils.ex index 8f9515e..d86abf7 100644 --- a/lib/ash_authentication/utils.ex +++ b/lib/ash_authentication/utils.ex @@ -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 diff --git a/lib/ash_authentication/validations/action.ex b/lib/ash_authentication/validations/action.ex index a1e7ef7..0e93615 100644 --- a/lib/ash_authentication/validations/action.ex +++ b/lib/ash_authentication/validations/action.ex @@ -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 diff --git a/lib/ash_authentication/validations/attribute.ex b/lib/ash_authentication/validations/attribute.ex index 20f2c2a..60b5972 100644 --- a/lib/ash_authentication/validations/attribute.ex +++ b/lib/ash_authentication/validations/attribute.ex @@ -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}" diff --git a/lib/ash_authentication/verifier.ex b/lib/ash_authentication/verifier.ex index 1832472..a7bb469 100644 --- a/lib/ash_authentication/verifier.ex +++ b/lib/ash_authentication/verifier.ex @@ -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 diff --git a/mix.exs b/mix.exs index 4b21848..78e4593 100644 --- a/mix.exs +++ b/mix.exs @@ -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]}, diff --git a/mix.lock b/mix.lock index 7519748..2f7505b 100644 --- a/mix.lock +++ b/mix.lock @@ -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"}, } diff --git a/priv/repo/migrations/20240328001848_install_2_extensions_wat.exs b/priv/repo/migrations/20240328001848_install_2_extensions_wat.exs new file mode 100644 index 0000000..c1038d1 --- /dev/null +++ b/priv/repo/migrations/20240328001848_install_2_extensions_wat.exs @@ -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 diff --git a/priv/repo/migrations/20240328002131_update_timestamp_defaults.exs b/priv/repo/migrations/20240328002131_update_timestamp_defaults.exs new file mode 100644 index 0000000..9e6b311 --- /dev/null +++ b/priv/repo/migrations/20240328002131_update_timestamp_defaults.exs @@ -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 diff --git a/priv/resource_snapshots/extensions.json b/priv/resource_snapshots/repo/extensions.json similarity index 100% rename from priv/resource_snapshots/extensions.json rename to priv/resource_snapshots/repo/extensions.json diff --git a/priv/resource_snapshots/repo/tokens/20240328002131.json b/priv/resource_snapshots/repo/tokens/20240328002131.json new file mode 100644 index 0000000..5e1019a --- /dev/null +++ b/priv/resource_snapshots/repo/tokens/20240328002131.json @@ -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 +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/user/20240328002131.json b/priv/resource_snapshots/repo/user/20240328002131.json new file mode 100644 index 0000000..18f6528 --- /dev/null +++ b/priv/resource_snapshots/repo/user/20240328002131.json @@ -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 +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/user_identities/20240328002131.json b/priv/resource_snapshots/repo/user_identities/20240328002131.json new file mode 100644 index 0000000..a34abd2 --- /dev/null +++ b/priv/resource_snapshots/repo/user_identities/20240328002131.json @@ -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 +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/user_with_token_required/20240328002131.json b/priv/resource_snapshots/repo/user_with_token_required/20240328002131.json new file mode 100644 index 0000000..36a9bb9 --- /dev/null +++ b/priv/resource_snapshots/repo/user_with_token_required/20240328002131.json @@ -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 +} \ No newline at end of file diff --git a/test/ash_authentication/add_ons/confirmation/actions_test.exs b/test/ash_authentication/add_ons/confirmation/actions_test.exs index 3ee5b42..aa310b3 100644 --- a/test/ash_authentication/add_ons/confirmation/actions_test.exs +++ b/test/ash_authentication/add_ons/confirmation/actions_test.exs @@ -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 diff --git a/test/ash_authentication/strategies/password/actions_test.exs b/test/ash_authentication/strategies/password/actions_test.exs index f15dde5..d071eed 100644 --- a/test/ash_authentication/strategies/password/actions_test.exs +++ b/test/ash_authentication/strategies/password/actions_test.exs @@ -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} diff --git a/test/ash_authentication/strategies/password/hash_password_change_test.exs b/test/ash_authentication/strategies/password/hash_password_change_test.exs index 2d32384..e24af3c 100644 --- a/test/ash_authentication/strategies/password/hash_password_change_test.exs +++ b/test/ash_authentication/strategies/password/hash_password_change_test.exs @@ -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 -> diff --git a/test/ash_authentication/strategies/password/password_confirmation_validation_test.exs b/test/ash_authentication/strategies/password/password_confirmation_validation_test.exs index 2b42e1b..e483dc4 100644 --- a/test/ash_authentication/strategies/password/password_confirmation_validation_test.exs +++ b/test/ash_authentication/strategies/password/password_confirmation_validation_test.exs @@ -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 diff --git a/test/ash_authentication/strategies/password/password_validation_test.exs b/test/ash_authentication/strategies/password/password_validation_test.exs index ecd524d..776c1ef 100644 --- a/test/ash_authentication/strategies/password/password_validation_test.exs +++ b/test/ash_authentication/strategies/password/password_validation_test.exs @@ -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 diff --git a/test/support/data_case.ex b/test/support/data_case.ex index 782cada..3827104 100644 --- a/test/support/data_case.ex +++ b/test/support/data_case.ex @@ -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 -> diff --git a/test/support/example.ex b/test/support/example.ex index 5c61bdb..f55fa0f 100644 --- a/test/support/example.ex +++ b/test/support/example.ex @@ -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 diff --git a/test/support/example/only_marties_at_the_party.ex b/test/support/example/only_marties_at_the_party.ex index f0a1da7..9b5064c 100644 --- a/test/support/example/only_marties_at_the_party.ex +++ b/test/support/example/only_marties_at_the_party.ex @@ -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} diff --git a/test/support/example/schema.ex b/test/support/example/schema.ex index 6545bfe..ac024b8 100644 --- a/test/support/example/schema.ex +++ b/test/support/example/schema.ex @@ -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 diff --git a/test/support/example/token.ex b/test/support/example/token.ex index 15edb80..b089884 100644 --- a/test/support/example/token.ex +++ b/test/support/example/token.ex @@ -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 diff --git a/test/support/example/user.ex b/test/support/example/user.ex index 1a3042d..55aa92d 100644 --- a/test/support/example/user.ex +++ b/test/support/example/user.ex @@ -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 diff --git a/test/support/example/user_identity.ex b/test/support/example/user_identity.ex index a3a78f2..15d0893 100644 --- a/test/support/example/user_identity.ex +++ b/test/support/example/user_identity.ex @@ -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 diff --git a/test/support/example/user_with_token_required.ex b/test/support/example/user_with_token_required.ex index e4cd6ad..d929e4b 100644 --- a/test/support/example/user_with_token_required.ex +++ b/test/support/example/user_with_token_required.ex @@ -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