mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-19 21:03:23 +12:00
improvement!: Update to support Ash 3.0. (#599)
This commit is contained in:
parent
71d510efc6
commit
f0075e2cd3
93 changed files with 1200 additions and 625 deletions
|
@ -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}"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
<h2>Resources:</h2>
|
||||
|
||||
<%= for {resource, options, strategies} <- @resources do %>
|
||||
<h2><%= inspect(options.subject_name) %> - <%= Ash.Api.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2>
|
||||
<h2><%= inspect(options.subject_name) %> - <%= Ash.Domain.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2>
|
||||
|
||||
|
||||
<%= for strategy <- strategies do %>
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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. |
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
# ...
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
# ...
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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([])}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}`
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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} ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}"
|
||||
|
|
|
@ -17,30 +17,31 @@ defmodule AshAuthentication.Verifier do
|
|||
| {:error, term}
|
||||
| {:warn, String.t() | list(String.t())}
|
||||
def verify(dsl_state) do
|
||||
with {:ok, _api} <- validate_api_presence(dsl_state) do
|
||||
with {:ok, _domain} <- validate_domain_presence(dsl_state) do
|
||||
validate_token_resource(dsl_state)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_presence(dsl_state) do
|
||||
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:authentication], :api),
|
||||
:ok <- assert_is_module(api),
|
||||
true <- function_exported?(api, :spark_is, 0),
|
||||
Ash.Api <- api.spark_is() do
|
||||
{:ok, api}
|
||||
defp validate_domain_presence(dsl_state) do
|
||||
with domain when not is_nil(domain) <-
|
||||
Transformer.get_option(dsl_state, [:authentication], :domain),
|
||||
:ok <- assert_is_module(domain),
|
||||
true <- function_exported?(domain, :spark_is, 0),
|
||||
Ash.Domain <- domain.spark_is() do
|
||||
{:ok, domain}
|
||||
else
|
||||
nil ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :api],
|
||||
message: "An API module must be present"
|
||||
path: [:authentication, :domain],
|
||||
message: "A domain module must be present"
|
||||
)}
|
||||
|
||||
_ ->
|
||||
{:error,
|
||||
DslError.exception(
|
||||
path: [:authentication, :api],
|
||||
message: "Module is not an Ash.Api."
|
||||
path: [:authentication, :domain],
|
||||
message: "Module is not an `Ash.Domain`."
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
|
13
mix.exs
13
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]},
|
||||
|
|
29
mix.lock
29
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"},
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
89
priv/resource_snapshots/repo/tokens/20240328002131.json
Normal file
89
priv/resource_snapshots/repo/tokens/20240328002131.json
Normal file
|
@ -0,0 +1,89 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "updated_at",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "created_at",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "map",
|
||||
"source": "extra_data",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "purpose",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "utc_datetime",
|
||||
"source": "expires_at",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "subject",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "jti",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": true
|
||||
}
|
||||
],
|
||||
"table": "tokens",
|
||||
"hash": "AE2F09DE877864EE3A814D7B2D24A41B88B79C869BEAD92907E6A980301E7ADC",
|
||||
"repo": "Elixir.Example.Repo",
|
||||
"identities": [],
|
||||
"schema": null,
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true
|
||||
}
|
109
priv/resource_snapshots/repo/user/20240328002131.json
Normal file
109
priv/resource_snapshots/repo/user/20240328002131.json
Normal file
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "confirmed_at",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"size": null,
|
||||
"type": "uuid",
|
||||
"source": "id",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": true
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "citext",
|
||||
"source": "username",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "extra_stuff",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "not_accepted_extra_stuff",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "hashed_password",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "created_at",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "updated_at",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
}
|
||||
],
|
||||
"table": "user",
|
||||
"hash": "2B26A24059722F798F9921CCB4E7E954478B920107F240EF313EE35CCCCEC1F4",
|
||||
"repo": "Elixir.Example.Repo",
|
||||
"identities": [
|
||||
{
|
||||
"name": "username",
|
||||
"keys": [
|
||||
"username"
|
||||
],
|
||||
"all_tenants?": false,
|
||||
"index_name": "user_username_index",
|
||||
"base_filter": null
|
||||
}
|
||||
],
|
||||
"schema": null,
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true
|
||||
}
|
119
priv/resource_snapshots/repo/user_identities/20240328002131.json
Normal file
119
priv/resource_snapshots/repo/user_identities/20240328002131.json
Normal file
|
@ -0,0 +1,119 @@
|
|||
{
|
||||
"attributes": [
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "refresh_token",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "utc_datetime_usec",
|
||||
"source": "access_token_expires_at",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "access_token",
|
||||
"references": null,
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "uid",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "text",
|
||||
"source": "strategy",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
},
|
||||
{
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"size": null,
|
||||
"type": "uuid",
|
||||
"source": "id",
|
||||
"references": null,
|
||||
"allow_nil?": false,
|
||||
"generated?": false,
|
||||
"primary_key?": true
|
||||
},
|
||||
{
|
||||
"default": "nil",
|
||||
"size": null,
|
||||
"type": "uuid",
|
||||
"source": "user_id",
|
||||
"references": {
|
||||
"name": "user_identities_user_id_fkey",
|
||||
"table": "user",
|
||||
"schema": "public",
|
||||
"on_delete": null,
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"primary_key?": true,
|
||||
"destination_attribute": "id",
|
||||
"deferrable": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"on_update": null,
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null
|
||||
},
|
||||
"allow_nil?": true,
|
||||
"generated?": false,
|
||||
"primary_key?": false
|
||||
}
|
||||
],
|
||||
"table": "user_identities",
|
||||
"hash": "9ED472E910FD711BEFFEAAACF1199466AB34B7FF41C5C076F1A95188FC3D60C6",
|
||||
"repo": "Elixir.Example.Repo",
|
||||
"identities": [
|
||||
{
|
||||
"name": "unique_on_strategy_and_uid_and_user_id",
|
||||
"keys": [
|
||||
"strategy",
|
||||
"uid",
|
||||
"user_id"
|
||||
],
|
||||
"all_tenants?": false,
|
||||
"index_name": "user_identities_unique_on_strategy_and_uid_and_user_id_index",
|
||||
"base_filter": null
|
||||
}
|
||||
],
|
||||
"schema": null,
|
||||
"multitenancy": {
|
||||
"global": null,
|
||||
"strategy": null,
|
||||
"attribute": null
|
||||
},
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue