mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-20 05:13:10 +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 = [
|
spark_locals_without_parens = [
|
||||||
access_token_attribute_name: 1,
|
access_token_attribute_name: 1,
|
||||||
access_token_expires_at_attribute_name: 1,
|
access_token_expires_at_attribute_name: 1,
|
||||||
api: 1,
|
|
||||||
auth0: 0,
|
auth0: 0,
|
||||||
auth0: 1,
|
auth0: 1,
|
||||||
auth0: 2,
|
auth0: 2,
|
||||||
|
@ -21,6 +20,7 @@ spark_locals_without_parens = [
|
||||||
confirmation_required?: 1,
|
confirmation_required?: 1,
|
||||||
confirmed_at_field: 1,
|
confirmed_at_field: 1,
|
||||||
destroy_action_name: 1,
|
destroy_action_name: 1,
|
||||||
|
domain: 1,
|
||||||
enabled?: 1,
|
enabled?: 1,
|
||||||
expunge_expired_action_name: 1,
|
expunge_expired_action_name: 1,
|
||||||
expunge_interval: 1,
|
expunge_interval: 1,
|
||||||
|
@ -106,7 +106,8 @@ spark_locals_without_parens = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[
|
[
|
||||||
import_deps: [:ash, :spark, :ash_json_api, :ash_graphql],
|
# , :ash_json_api, :ash_graphql],
|
||||||
|
import_deps: [:ash, :spark],
|
||||||
inputs: [
|
inputs: [
|
||||||
"*.{ex,exs}",
|
"*.{ex,exs}",
|
||||||
"{dev,config,lib,test}/**/*.{ex,exs}"
|
"{dev,config,lib,test}/**/*.{ex,exs}"
|
||||||
|
|
|
@ -13,7 +13,7 @@ config :git_ops,
|
||||||
|
|
||||||
config :ash_authentication, DevServer, start?: true, port: 4000
|
config :ash_authentication, DevServer, start?: true, port: 4000
|
||||||
|
|
||||||
config :ash_authentication, ecto_repos: [Example.Repo], ash_apis: [Example]
|
config :ash_authentication, ecto_repos: [Example.Repo], ash_domains: [Example]
|
||||||
|
|
||||||
config :ash_authentication, Example.Repo,
|
config :ash_authentication, Example.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :ash_authentication, ecto_repos: [Example.Repo], ash_apis: [Example]
|
config :ash_authentication, ecto_repos: [Example.Repo], ash_domains: [Example]
|
||||||
|
|
||||||
config :ash_authentication, Example.Repo,
|
config :ash_authentication, Example.Repo,
|
||||||
username: "postgres",
|
username: "postgres",
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
defmodule DevServer.ApiRouter do
|
# defmodule DevServer.ApiRouter do
|
||||||
@moduledoc """
|
# @moduledoc """
|
||||||
Router for API Requests.
|
# Router for API Requests.
|
||||||
"""
|
# """
|
||||||
use Plug.Router
|
# use Plug.Router
|
||||||
import Example.AuthPlug
|
# import Example.AuthPlug
|
||||||
|
|
||||||
plug(:load_from_bearer)
|
# plug(:load_from_bearer)
|
||||||
plug(:set_actor, :user)
|
# plug(:set_actor, :user)
|
||||||
plug(:match)
|
# plug(:match)
|
||||||
plug(:dispatch)
|
# plug(:dispatch)
|
||||||
|
|
||||||
forward("/", to: DevServer.JsonApiRouter)
|
# forward("/", to: DevServer.JsonApiRouter)
|
||||||
end
|
# end
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
defmodule DevServer.GqlRouter do
|
# defmodule DevServer.GqlRouter do
|
||||||
@moduledoc """
|
# @moduledoc """
|
||||||
Router for GraphQL requests.
|
# Router for GraphQL requests.
|
||||||
"""
|
# """
|
||||||
use Plug.Router
|
# use Plug.Router
|
||||||
import Example.AuthPlug
|
# import Example.AuthPlug
|
||||||
|
|
||||||
plug(:load_from_bearer)
|
# plug(:load_from_bearer)
|
||||||
plug(:set_actor, :user)
|
# plug(:set_actor, :user)
|
||||||
plug(AshGraphql.Plug)
|
# plug(AshGraphql.Plug)
|
||||||
plug(:match)
|
# plug(:match)
|
||||||
plug(:dispatch)
|
# plug(:dispatch)
|
||||||
|
|
||||||
forward("/playground",
|
# forward("/playground",
|
||||||
to: Absinthe.Plug.GraphiQL,
|
# to: Absinthe.Plug.GraphiQL,
|
||||||
init_opts: [
|
# init_opts: [
|
||||||
schema: Example.Schema,
|
# schema: Example.Schema,
|
||||||
interface: :playground
|
# interface: :playground
|
||||||
]
|
# ]
|
||||||
)
|
# )
|
||||||
|
|
||||||
forward("/",
|
# forward("/",
|
||||||
to: Absinthe.Plug,
|
# to: Absinthe.Plug,
|
||||||
init_opts: [schema: Example.Schema]
|
# init_opts: [schema: Example.Schema]
|
||||||
)
|
# )
|
||||||
end
|
# end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule DevServer.JsonApiRouter do
|
# defmodule DevServer.JsonApiRouter do
|
||||||
@moduledoc false
|
# @moduledoc false
|
||||||
use AshJsonApi.Api.Router, api: Example
|
# use AshJsonApi.Api.Router, api: Example
|
||||||
end
|
# end
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule DevServer.Router do
|
||||||
forward("/auth", to: Example.AuthPlug)
|
forward("/auth", to: Example.AuthPlug)
|
||||||
get("/clear_session", to: DevServer.ClearSession)
|
get("/clear_session", to: DevServer.ClearSession)
|
||||||
post("/token_check", to: DevServer.TokenCheck)
|
post("/token_check", to: DevServer.TokenCheck)
|
||||||
forward("/api", to: DevServer.ApiRouter)
|
# forward("/api", to: DevServer.ApiRouter)
|
||||||
forward("/gql", to: DevServer.GqlRouter)
|
# forward("/gql", to: DevServer.GqlRouter)
|
||||||
forward("/", to: DevServer.WebRouter)
|
forward("/", to: DevServer.WebRouter)
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<h2>Resources:</h2>
|
<h2>Resources:</h2>
|
||||||
|
|
||||||
<%= for {resource, options, strategies} <- @resources do %>
|
<%= for {resource, options, strategies} <- @resources do %>
|
||||||
<h2><%= inspect(options.subject_name) %> - <%= Ash.Api.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2>
|
<h2><%= inspect(options.subject_name) %> - <%= Ash.Domain.Info.short_name(options.api) %> / <%= Ash.Resource.Info.short_name(resource) %></h2>
|
||||||
|
|
||||||
|
|
||||||
<%= for strategy <- strategies do %>
|
<%= for strategy <- strategies do %>
|
||||||
|
|
|
@ -22,7 +22,8 @@ minimum requirements:
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -30,8 +31,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
add_ons do
|
add_ons do
|
||||||
confirmation :confirm do
|
confirmation :confirm do
|
||||||
monitor_fields [:email]
|
monitor_fields [:email]
|
||||||
|
|
|
@ -19,7 +19,8 @@ There are other options documented in the DSL.
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -27,8 +28,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
magic_link do
|
magic_link do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
|
|
@ -20,7 +20,8 @@ the following minimum criteria:
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -28,8 +29,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
client_id "OAuth Client ID"
|
client_id "OAuth Client ID"
|
||||||
|
@ -158,8 +157,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
registration_enabled? false
|
registration_enabled? false
|
||||||
|
@ -196,8 +193,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,7 +19,8 @@ There are other options documented in the DSL.
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -28,8 +29,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
|
|
@ -30,11 +30,8 @@ system to function.
|
||||||
defmodule MyApp.Accounts.Token do
|
defmodule MyApp.Accounts.Token do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.TokenResource]
|
extensions: [AshAuthentication.TokenResource],
|
||||||
|
domain: MyApp.Accounts
|
||||||
token do
|
|
||||||
api MyApp.Accounts
|
|
||||||
end
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "tokens"
|
table "tokens"
|
||||||
|
@ -70,7 +67,7 @@ Configuration options for this token resource
|
||||||
|
|
||||||
| Name | Type | Default | Docs |
|
| Name | Type | Default | Docs |
|
||||||
|------|------|---------|------|
|
|------|------|---------|------|
|
||||||
| [`api`](#token-api){: #token-api } | `module` | | The Ash API to use to access this resource. |
|
| [`domain`](#token-domain){: #token-domain } | `module` | | The Ash domain to use to access this resource. |
|
||||||
| [`expunge_expired_action_name`](#token-expunge_expired_action_name){: #token-expunge_expired_action_name } | `atom` | `:expunge_expired` | The name of the action used to remove expired tokens. |
|
| [`expunge_expired_action_name`](#token-expunge_expired_action_name){: #token-expunge_expired_action_name } | `atom` | `:expunge_expired` | The name of the action used to remove expired tokens. |
|
||||||
| [`read_expired_action_name`](#token-read_expired_action_name){: #token-read_expired_action_name } | `atom` | `:read_expired` | The name of the action use to find all expired tokens. |
|
| [`read_expired_action_name`](#token-read_expired_action_name){: #token-read_expired_action_name } | `atom` | `:read_expired` | The name of the action use to find all expired tokens. |
|
||||||
| [`expunge_interval`](#token-expunge_interval){: #token-expunge_interval } | `pos_integer` | `12` | How often to scan this resource for records which have expired, and thus can be removed. |
|
| [`expunge_interval`](#token-expunge_interval){: #token-expunge_interval } | `pos_integer` | `12` | How often to scan this resource for records which have expired, and thus can be removed. |
|
||||||
|
|
|
@ -29,10 +29,10 @@ unlikely that you will need to customise it.
|
||||||
defmodule MyApp.Accounts.UserIdentity do
|
defmodule MyApp.Accounts.UserIdentity do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.UserIdentity]
|
extensions: [AshAuthentication.UserIdentity],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
user_identity do
|
user_identity do
|
||||||
api MyApp.Accounts
|
|
||||||
user_resource MyApp.Accounts.User
|
user_resource MyApp.Accounts.User
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ Configure identity options for this resource
|
||||||
| Name | Type | Default | Docs |
|
| Name | Type | Default | Docs |
|
||||||
|------|------|---------|------|
|
|------|------|---------|------|
|
||||||
| [`user_resource`](#user_identity-user_resource){: #user_identity-user_resource .spark-required} | `module` | | The user resource to which these identities belong. |
|
| [`user_resource`](#user_identity-user_resource){: #user_identity-user_resource .spark-required} | `module` | | The user resource to which these identities belong. |
|
||||||
| [`api`](#user_identity-api){: #user_identity-api } | `module` | | The Ash API to use to access this resource. |
|
| [`domain`](#user_identity-domain){: #user_identity-domain } | `module` | | The Ash domain to use to access this resource. |
|
||||||
| [`uid_attribute_name`](#user_identity-uid_attribute_name){: #user_identity-uid_attribute_name } | `atom` | `:uid` | The name of the `uid` attribute on this resource. |
|
| [`uid_attribute_name`](#user_identity-uid_attribute_name){: #user_identity-uid_attribute_name } | `atom` | `:uid` | The name of the `uid` attribute on this resource. |
|
||||||
| [`strategy_attribute_name`](#user_identity-strategy_attribute_name){: #user_identity-strategy_attribute_name } | `atom` | `:strategy` | The name of the `strategy` attribute on this resource. |
|
| [`strategy_attribute_name`](#user_identity-strategy_attribute_name){: #user_identity-strategy_attribute_name } | `atom` | `:strategy` | The name of the `strategy` attribute on this resource. |
|
||||||
| [`user_id_attribute_name`](#user_identity-user_id_attribute_name){: #user_identity-user_id_attribute_name } | `atom` | `:user_id` | The name of the `user_id` attribute on this resource. |
|
| [`user_id_attribute_name`](#user_identity-user_id_attribute_name){: #user_identity-user_id_attribute_name } | `atom` | `:user_id` | The name of the `user_id` attribute on this resource. |
|
||||||
|
|
|
@ -17,7 +17,8 @@ the `AshAuthentication` extension on your resource:
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -26,8 +27,6 @@ defmodule MyApp.Accounts.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
@ -101,7 +100,7 @@ Configure authentication for this resource
|
||||||
| Name | Type | Default | Docs |
|
| Name | Type | Default | Docs |
|
||||||
|------|------|---------|------|
|
|------|------|---------|------|
|
||||||
| [`subject_name`](#authentication-subject_name){: #authentication-subject_name } | `atom` | | The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`). |
|
| [`subject_name`](#authentication-subject_name){: #authentication-subject_name } | `atom` | | The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`). |
|
||||||
| [`api`](#authentication-api){: #authentication-api } | `module` | | The name of the Ash API to use to access this resource when doing anything authenticaiton related. |
|
| [`domain`](#authentication-domain){: #authentication-domain } | `module` | | The name of the Ash domain to use to access this resource when doing anything authentication related. |
|
||||||
| [`get_by_subject_action_name`](#authentication-get_by_subject_action_name){: #authentication-get_by_subject_action_name } | `atom` | `:get_by_subject` | The name of the read action used to retrieve records. If the action doesn't exist, one will be generated for you. |
|
| [`get_by_subject_action_name`](#authentication-get_by_subject_action_name){: #authentication-get_by_subject_action_name } | `atom` | `:get_by_subject` | The name of the read action used to retrieve records. If the action doesn't exist, one will be generated for you. |
|
||||||
| [`select_for_senders`](#authentication-select_for_senders){: #authentication-select_for_senders } | `list(atom)` | | A list of fields that we will ensure are selected whenever a sender will be invoked. Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]` otherwise. |
|
| [`select_for_senders`](#authentication-select_for_senders){: #authentication-select_for_senders } | `list(atom)` | | A list of fields that we will ensure are selected whenever a sender will be invoked. Defaults to `[:email]` if there is an `:email` attribute on the resource, and `[]` otherwise. |
|
||||||
|
|
||||||
|
|
|
@ -4,30 +4,30 @@ AshAuthentication allows you to bring your own authentication strategy without
|
||||||
having to change the Ash Authentication codebase.
|
having to change the Ash Authentication codebase.
|
||||||
|
|
||||||
> There is functionally no difference between "add ons" and "strategies" other
|
> There is functionally no difference between "add ons" and "strategies" other
|
||||||
> than where they appear in the DSL. We invented "add ons" because it felt
|
> than where they appear in the DSL. We invented "add ons" because it felt
|
||||||
> weird calling "confirmation" an authentication strategy.
|
> weird calling "confirmation" an authentication strategy.
|
||||||
|
|
||||||
There are several moving parts which must all work together so hold on to your hat!
|
There are several moving parts which must all work together so hold on to your hat!
|
||||||
|
|
||||||
1. A `Spark.Dsl.Entity` struct. This is used to define the strategy DSL
|
1. A `Spark.Dsl.Entity` struct. This is used to define the strategy DSL
|
||||||
inside the `strategies` (or `add_ons`) section of the `authentication` DSL.
|
inside the `strategies` (or `add_ons`) section of the `authentication` DSL.
|
||||||
2. A strategy struct, which stores information about the strategy as
|
2. A strategy struct, which stores information about the strategy as
|
||||||
configured on a resource which must comply with a few rules.
|
configured on a resource which must comply with a few rules.
|
||||||
3. An optional transformer, which can be used to manipulate the DSL state of
|
3. An optional transformer, which can be used to manipulate the DSL state of
|
||||||
the entity and the resource.
|
the entity and the resource.
|
||||||
4. An optional verifier, which can be used to verify the DSL state of the
|
4. An optional verifier, which can be used to verify the DSL state of the
|
||||||
entity and the resource after compilation.
|
entity and the resource after compilation.
|
||||||
4. The `AshAuthentication.Strategy` protocol, which provides the glue needed
|
5. The `AshAuthentication.Strategy` protocol, which provides the glue needed
|
||||||
for everything to wire up and wrappers around the actions needed to run on
|
for everything to wire up and wrappers around the actions needed to run on
|
||||||
the resource.
|
the resource.
|
||||||
|
|
||||||
We're going to define an extremely dumb strategy which lets anyone with a name
|
We're going to define an extremely dumb strategy which lets anyone with a name
|
||||||
that starts with "Marty" sign in with just their name. Of course you would
|
that starts with "Marty" sign in with just their name. Of course you would
|
||||||
never do this in real life, but this isn't real life - it's documentation!
|
never do this in real life, but this isn't real life - it's documentation!
|
||||||
|
|
||||||
## DSL setup
|
## DSL setup
|
||||||
|
|
||||||
Let's start by defining a module for our strategy to live in. Let's call it
|
Let's start by defining a module for our strategy to live in. Let's call it
|
||||||
`OnlyMartiesAtTheParty`:
|
`OnlyMartiesAtTheParty`:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
@ -36,7 +36,7 @@ defmodule OnlyMartiesAtTheParty do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Sadly, this isn't enough to make the magic happen. We need to define our DSL
|
Sadly, this isn't enough to make the magic happen. We need to define our DSL
|
||||||
entity by adding it to the `use` statement:
|
entity by adding it to the `use` statement:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
@ -87,22 +87,22 @@ end
|
||||||
If you haven't you should take a look at the docs for `Spark.Dsl.Entity`, but
|
If you haven't you should take a look at the docs for `Spark.Dsl.Entity`, but
|
||||||
here's a brief overview of what each field we've set does:
|
here's a brief overview of what each field we've set does:
|
||||||
|
|
||||||
- `name` is the name for which the helper function will be generated in
|
- `name` is the name for which the helper function will be generated in
|
||||||
the DSL (ie `only_marty do #... end`).
|
the DSL (ie `only_marty do #... end`).
|
||||||
- `describe` and `examples` are used when generating documentation.
|
- `describe` and `examples` are used when generating documentation.
|
||||||
- `target` is the name of the module which defines our entity struct. We've
|
- `target` is the name of the module which defines our entity struct. We've
|
||||||
set it to `__MODULE__` which means that we'll have to define the struct on
|
set it to `__MODULE__` which means that we'll have to define the struct on
|
||||||
this module.
|
this module.
|
||||||
- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark
|
- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark
|
||||||
provides a number of additional types over the default ones though, so check
|
provides a number of additional types over the default ones though, so check
|
||||||
out `Spark.OptionsHelpers` for more information.
|
out `Spark.OptionsHelpers` for more information.
|
||||||
|
|
||||||
> By default the entity is added to the `authentication / strategy` DSL, however
|
> By default the entity is added to the `authentication / strategy` DSL, however
|
||||||
> if you want it in the `authentication / add_ons` DSL instead you can also pass
|
> if you want it in the `authentication / add_ons` DSL instead you can also pass
|
||||||
> `style: :add_on` in the `use` statement.
|
> `style: :add_on` in the `use` statement.
|
||||||
|
|
||||||
Next up, we need to define our struct. The struct should have *at least* the
|
Next up, we need to define our struct. The struct should have _at least_ the
|
||||||
fields named in the entity schema. Additionally, Ash Authentication requires
|
fields named in the entity schema. Additionally, Ash Authentication requires
|
||||||
that it have a `resource` field which will be set to the module of the resource
|
that it have a `resource` field which will be set to the module of the resource
|
||||||
it's attached to during compilation.
|
it's attached to during compilation.
|
||||||
|
|
||||||
|
@ -123,11 +123,11 @@ by adding it to the `extensions` section of your resource:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication, OnlyMartiesAtTheParty]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication, OnlyMartiesAtTheParty],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
only_marty do
|
only_marty do
|
||||||
name_field :name
|
name_field :name
|
||||||
|
@ -145,25 +145,25 @@ end
|
||||||
## Implementing the `AshAuthentication.Strategy` protocol
|
## Implementing the `AshAuthentication.Strategy` protocol
|
||||||
|
|
||||||
The Strategy protocol is used to introspect the strategy so that it can
|
The Strategy protocol is used to introspect the strategy so that it can
|
||||||
seamlessly fit in with the rest of Ash Authentication. Here are the key
|
seamlessly fit in with the rest of Ash Authentication. Here are the key
|
||||||
concepts:
|
concepts:
|
||||||
|
|
||||||
- "phases" - in terms of HTTP, each strategy is likely to have many phases (eg
|
- "phases" - in terms of HTTP, each strategy is likely to have many phases (eg
|
||||||
OAuth 2.0's "request" and "callback" phases). Essentially you need one
|
OAuth 2.0's "request" and "callback" phases). Essentially you need one
|
||||||
phase for each HTTP endpoint you wish to support with your strategy. In our
|
phase for each HTTP endpoint you wish to support with your strategy. In our
|
||||||
case we just want one sign in endpoint.
|
case we just want one sign in endpoint.
|
||||||
- "actions" - actions are exactly as they sound - Resource actions which can
|
- "actions" - actions are exactly as they sound - Resource actions which can
|
||||||
be executed by the strategy, whether generated by the strategy (as in the
|
be executed by the strategy, whether generated by the strategy (as in the
|
||||||
password strategy) or typed in by the user (as in the OAuth 2.0 strategy).
|
password strategy) or typed in by the user (as in the OAuth 2.0 strategy).
|
||||||
The reason that we wrap the strategy's actions this way is that all the
|
The reason that we wrap the strategy's actions this way is that all the
|
||||||
built-in strategies (and we hope yours too) allow the user to customise the
|
built-in strategies (and we hope yours too) allow the user to customise the
|
||||||
name of the actions that it uses. At the very least it should probably
|
name of the actions that it uses. At the very least it should probably
|
||||||
append the strategy name to the action. Using `Strategy.action/4` allows us
|
append the strategy name to the action. Using `Strategy.action/4` allows us
|
||||||
to refer these by a more generic name rather than via the user-specified one
|
to refer these by a more generic name rather than via the user-specified one
|
||||||
(eg `:register` vs `:register_with_password`).
|
(eg `:register` vs `:register_with_password`).
|
||||||
- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`)
|
- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`)
|
||||||
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
|
will generate routes using `Plug.Router` (or `Phoenix.Router`) - the
|
||||||
`routes/1` callback is used to retrieve this information from the strategy.
|
`routes/1` callback is used to retrieve this information from the strategy.
|
||||||
|
|
||||||
Given this information, let's implement the strategy. It's quite long, so I'm
|
Given this information, let's implement the strategy. It's quite long, so I'm
|
||||||
going to break it up into smaller chunks.
|
going to break it up into smaller chunks.
|
||||||
|
@ -172,8 +172,8 @@ going to break it up into smaller chunks.
|
||||||
defimpl AshAuthentication.Strategy, for: OnlyMartiesAtTheParty do
|
defimpl AshAuthentication.Strategy, for: OnlyMartiesAtTheParty do
|
||||||
```
|
```
|
||||||
|
|
||||||
The `name/1` function is used to uniquely identify the strategy. It *must* be an
|
The `name/1` function is used to uniquely identify the strategy. It _must_ be an
|
||||||
atom and *should* be the same as the path fragment used in the generated routes.
|
atom and _should_ be the same as the path fragment used in the generated routes.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
def name(strategy), do: strategy.name
|
def name(strategy), do: strategy.name
|
||||||
|
@ -187,7 +187,7 @@ and action.
|
||||||
def actions(_), do: [:sign_in]
|
def actions(_), do: [:sign_in]
|
||||||
```
|
```
|
||||||
|
|
||||||
Next we generate the routes for the strategy. Routes *should* contain the
|
Next we generate the routes for the strategy. Routes _should_ contain the
|
||||||
subject name of the resource being authenticated in case the implementer is
|
subject name of the resource being authenticated in case the implementer is
|
||||||
authenticating multiple different resources - eg `User` and `Admin`.
|
authenticating multiple different resources - eg `User` and `Admin`.
|
||||||
|
|
||||||
|
@ -207,8 +207,8 @@ When generating routes or forms for this phase, what HTTP method should we use?
|
||||||
def method_for_phase(_, :sign_in), do: :post
|
def method_for_phase(_, :sign_in), do: :post
|
||||||
```
|
```
|
||||||
|
|
||||||
Next up, we write our plug. We take the "name field" from the input params in
|
Next up, we write our plug. We take the "name field" from the input params in
|
||||||
the conn and pass them to our sign in action. As long as the action returns
|
the conn and pass them to our sign in action. As long as the action returns
|
||||||
`{:ok, Ash.Resource.record}` or `{:error, any}` then we can just pass it
|
`{:ok, Ash.Resource.record}` or `{:error, any}` then we can just pass it
|
||||||
straight into `store_authentication_result/2` from
|
straight into `store_authentication_result/2` from
|
||||||
`AshAuthentication.Plug.Helpers`.
|
`AshAuthentication.Plug.Helpers`.
|
||||||
|
@ -223,11 +223,11 @@ straight into `store_authentication_result/2` from
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally, we implement our sign in action. We use `Ash.Query` to find all
|
Finally, we implement our sign in action. We use `Ash.Query` to find all
|
||||||
records whose name field matches the input, then constrain it to only records
|
records whose name field matches the input, then constrain it to only records
|
||||||
whose name field starts with "Marty". Depending on whether the name field has a
|
whose name field starts with "Marty". Depending on whether the name field has a
|
||||||
unique identity on it we have to deal with it returning zero or more users, or
|
unique identity on it we have to deal with it returning zero or more users, or
|
||||||
an error. When it returns a single user we return that user in an ok tuple,
|
an error. When it returns a single user we return that user in an ok tuple,
|
||||||
otherwise we return an authentication failure.
|
otherwise we return an authentication failure.
|
||||||
|
|
||||||
In this example we're assuming that there is a default `read` action present on
|
In this example we're assuming that there is a default `read` action present on
|
||||||
|
@ -246,22 +246,23 @@ the resource.
|
||||||
```elixir
|
```elixir
|
||||||
alias AshAuthentication.Errors.AuthenticationFailed
|
alias AshAuthentication.Errors.AuthenticationFailed
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
import Ash.Expr
|
||||||
|
|
||||||
def action(strategy, :sign_in, params, options) do
|
def action(strategy, :sign_in, params, options) do
|
||||||
name_field = strategy.name_field
|
name_field = strategy.name_field
|
||||||
name = Map.get(params, to_string(name_field))
|
name = Map.get(params, to_string(name_field))
|
||||||
api = AshAuthentication.Info.authentication_api!(strategy.resource)
|
domain = AshAuthentication.Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Ash.Query.filter(ref(^name_field) == ^name)
|
|> Ash.Query.filter(expr(^ref(name_field) == ^name))
|
||||||
|> then(fn query ->
|
|> then(fn query ->
|
||||||
if strategy.case_sensitive? do
|
if strategy.case_sensitive? do
|
||||||
Ash.Query.filter(query, like(ref(^name_field), "Marty%"))
|
Ash.Query.filter(query, like(^ref(name_field), "Marty%"))
|
||||||
else
|
else
|
||||||
Ash.Query.filter(query, ilike(ref(^name_field), "Marty%"))
|
Ash.Query.filter(query, ilike(^ref(name_field), "Marty%"))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
@ -282,7 +283,7 @@ end
|
||||||
## Bonus round - transformers and verifiers
|
## Bonus round - transformers and verifiers
|
||||||
|
|
||||||
In some cases it may be required for your strategy to modify it's own
|
In some cases it may be required for your strategy to modify it's own
|
||||||
configuration or that of the whole resource at compile time. For that you can
|
configuration or that of the whole resource at compile time. For that you can
|
||||||
define the `transform/2` callback on your strategy module.
|
define the `transform/2` callback on your strategy module.
|
||||||
|
|
||||||
At the very least it is good practice to call
|
At the very least it is good practice to call
|
||||||
|
@ -294,7 +295,7 @@ imported by `use AshAuthentication.Strategy.Custom` for this purpose.
|
||||||
### Transformers
|
### Transformers
|
||||||
|
|
||||||
For simple cases where you're just transforming the strategy you can just return
|
For simple cases where you're just transforming the strategy you can just return
|
||||||
the modified strategy and the DSL will be updated accordingly. For example if
|
the modified strategy and the DSL will be updated accordingly. For example if
|
||||||
you wanted to generate the name of an action if the user hasn't specified it:
|
you wanted to generate the name of an action if the user hasn't specified it:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
@ -303,9 +304,9 @@ def transform(strategy, _dsl_state) do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
In some cases you may want to modify the strategy and the resources DSL. In
|
In some cases you may want to modify the strategy and the resources DSL. In
|
||||||
this case you can return the newly mutated DSL state in an ok tuple or an error
|
this case you can return the newly mutated DSL state in an ok tuple or an error
|
||||||
tuple, preferably containing a `Spark.Error.DslError`. For example if we wanted
|
tuple, preferably containing a `Spark.Error.DslError`. For example if we wanted
|
||||||
to build a sign in action for `OnlyMartiesAtTheParty` to use:
|
to build a sign in action for `OnlyMartiesAtTheParty` to use:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
@ -332,12 +333,12 @@ end
|
||||||
```
|
```
|
||||||
|
|
||||||
Transformers can also be used to validate user input or even directly add code
|
Transformers can also be used to validate user input or even directly add code
|
||||||
to the resource. See the docs for `Spark.Dsl.Transformer` for more information.
|
to the resource. See the docs for `Spark.Dsl.Transformer` for more information.
|
||||||
|
|
||||||
### Verifiers
|
### Verifiers
|
||||||
|
|
||||||
We also support a variant of transformers which run in the new `@after_verify`
|
We also support a variant of transformers which run in the new `@after_verify`
|
||||||
compile hook provided by Elixir 1.14. This is a great place to put checks
|
compile hook provided by Elixir 1.14. This is a great place to put checks
|
||||||
to make sure that the user's configuration makes sense without adding any
|
to make sure that the user's configuration makes sense without adding any
|
||||||
compile-time dependencies between modules which may cause compiler deadlocks.
|
compile-time dependencies between modules which may cause compiler deadlocks.
|
||||||
|
|
||||||
|
|
|
@ -12,35 +12,39 @@ Before you start this tutorial, skip the Token resource while following the
|
||||||
Next, you need to configure an application in [the Auth0
|
Next, you need to configure an application in [the Auth0
|
||||||
dashboard](https://manage.auth0.com/) using the following steps:
|
dashboard](https://manage.auth0.com/) using the following steps:
|
||||||
|
|
||||||
1. Click "Create Application".
|
1. Click "Create Application".
|
||||||
2. Set your application name to something that identifies it. You will likely
|
2. Set your application name to something that identifies it. You will likely
|
||||||
need separate applications for development and production environments, so
|
need separate applications for development and production environments, so
|
||||||
keep that in mind.
|
keep that in mind.
|
||||||
3. Select "Regular Web Application" and click "Create".
|
3. Select "Regular Web Application" and click "Create".
|
||||||
4. Switch to the "Settings" tab.
|
4. Switch to the "Settings" tab.
|
||||||
5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll
|
5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll
|
||||||
need them soon.
|
need them soon.
|
||||||
6. In the "Allowed Callback URLs" section, add your callback URL. The
|
6. In the "Allowed Callback URLs" section, add your callback URL. The
|
||||||
callback URL is generated from the following information:
|
callback URL is generated from the following information:
|
||||||
- The base URL of the application - in development that would be
|
|
||||||
`http://localhost:4000/` but in production will be your application's
|
|
||||||
URL.
|
|
||||||
- The mount point of the auth routes in your router - we'll assume
|
|
||||||
`/auth`.
|
|
||||||
- The "subject name" of the resource being authenticated - we'll assume `user`.
|
|
||||||
- The name of the strategy in your configuration. By default this is
|
|
||||||
`auth0`.
|
|
||||||
|
|
||||||
This means that the callback URL should look something like
|
- The base URL of the application - in development that would be
|
||||||
`http://localhost:4000/auth/user/auth0/callback`.
|
`http://localhost:4000/` but in production will be your application's
|
||||||
7. Set "Allowed Web Origins" to your application's base URL.
|
URL.
|
||||||
8. Click "Save Changes".
|
- The mount point of the auth routes in your router - we'll assume
|
||||||
|
`/auth`.
|
||||||
|
- The "subject name" of the resource being authenticated - we'll assume `user`.
|
||||||
|
- The name of the strategy in your configuration. By default this is
|
||||||
|
`auth0`.
|
||||||
|
|
||||||
|
This means that the callback URL should look something like
|
||||||
|
`http://localhost:4000/auth/user/auth0/callback`.
|
||||||
|
|
||||||
|
7. Set "Allowed Web Origins" to your application's base URL.
|
||||||
|
8. Click "Save Changes".
|
||||||
|
|
||||||
Next we can configure our resource:
|
Next we can configure our resource:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
strategies do
|
strategies do
|
||||||
|
@ -92,29 +96,32 @@ end
|
||||||
|
|
||||||
The values for this configuration should be:
|
The values for this configuration should be:
|
||||||
|
|
||||||
* `client_id` - the client ID copied from the Auth0 settings page.
|
- `client_id` - the client ID copied from the Auth0 settings page.
|
||||||
* `redirect_uri` - the URL to the generated auth routes in your application
|
- `redirect_uri` - the URL to the generated auth routes in your application
|
||||||
(eg `http://localhost:4000/auth`).
|
(eg `http://localhost:4000/auth`).
|
||||||
* `client_secret` the client secret copied from the Auth0 settings page.
|
- `client_secret` the client secret copied from the Auth0 settings page.
|
||||||
* `base_url` - the "domain" value copied from the Auth0 settings page prefixed
|
- `base_url` - the "domain" value copied from the Auth0 settings page prefixed
|
||||||
with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`).
|
with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`).
|
||||||
|
|
||||||
Lastly, we need to add a register action to your user resource. This is defined
|
Lastly, we need to add a register action to your user resource. This is defined
|
||||||
as an upsert so that it can register new users, or update information for
|
as an upsert so that it can register new users, or update information for
|
||||||
returning users. The default name of the action is `register_with_` followed by
|
returning users. The default name of the action is `register_with_` followed by
|
||||||
the strategy name. In our case that is `register_with_auth0`.
|
the strategy name. In our case that is `register_with_auth0`.
|
||||||
|
|
||||||
The register action takes two arguments, `user_info` and the `oauth_tokens`.
|
The register action takes two arguments, `user_info` and the `oauth_tokens`.
|
||||||
- `user_info` contains the [`GET /userinfo` response from
|
|
||||||
Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you
|
- `user_info` contains the [`GET /userinfo` response from
|
||||||
can use to populate your user attributes as needed.
|
Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you
|
||||||
- `oauth_tokens` contains the [`POST /oauth/token` response from
|
can use to populate your user attributes as needed.
|
||||||
Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want
|
- `oauth_tokens` contains the [`POST /oauth/token` response from
|
||||||
to store these if you intend to call the Auth0 API on behalf of the user.
|
Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want
|
||||||
|
to store these if you intend to call the Auth0 API on behalf of the user.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ used when converting tokens or session information into a resource record.
|
||||||
### `AshAuthentication.Strategy.Password`
|
### `AshAuthentication.Strategy.Password`
|
||||||
|
|
||||||
This authentication strategy provides registration and sign-in for users using a local
|
This authentication strategy provides registration and sign-in for users using a local
|
||||||
identifier (eg `username`, `email` or `phone_number`) and a password. It will
|
identifier (eg `username`, `email` or `phone_number`) and a password. It will
|
||||||
define register and sign-in actions on your "user" resource. You are welcome to
|
define register and sign-in actions on your "user" resource. You are welcome to
|
||||||
define either or both of these actions yourself if you wish to customise them -
|
define either or both of these actions yourself if you wish to customise them -
|
||||||
if you do so then the extension will do it's best to validate that all required
|
if you do so then the extension will do it's best to validate that all required
|
||||||
configuration is present.
|
configuration is present.
|
||||||
|
@ -60,7 +60,7 @@ The `AshAuthentication.Strategy.Password` DSL allows you to override any of the
|
||||||
### `AshAuthentication.Strategy.OAuth2`
|
### `AshAuthentication.Strategy.OAuth2`
|
||||||
|
|
||||||
This authentication strategy provides registration and sign-in for users using a
|
This authentication strategy provides registration and sign-in for users using a
|
||||||
remote [OAuth 2.0](https://oauth.net/2/) server as the source of truth. You
|
remote [OAuth 2.0](https://oauth.net/2/) server as the source of truth. You
|
||||||
will be required to provide either a "register" or a "sign-in" action depending
|
will be required to provide either a "register" or a "sign-in" action depending
|
||||||
on your configuration, which the strategy will attempt to validate for common
|
on your configuration, which the strategy will attempt to validate for common
|
||||||
misconfigurations.
|
misconfigurations.
|
||||||
|
@ -74,7 +74,7 @@ change to take place.
|
||||||
### `AshAuthentication.TokenResource`
|
### `AshAuthentication.TokenResource`
|
||||||
|
|
||||||
This extension allows you to easily create a resource which will store
|
This extension allows you to easily create a resource which will store
|
||||||
information about tokens that can't be encoded into the tokens themselves. A
|
information about tokens that can't be encoded into the tokens themselves. A
|
||||||
resource with this extension must be present if token generation is enabled.
|
resource with this extension must be present if token generation is enabled.
|
||||||
|
|
||||||
### `AshAuthentication.UserIdentity`
|
### `AshAuthentication.UserIdentity`
|
||||||
|
@ -82,21 +82,21 @@ resource with this extension must be present if token generation is enabled.
|
||||||
If you plan to support multiple different strategies at once (eg giving your
|
If you plan to support multiple different strategies at once (eg giving your
|
||||||
users the choice of more than one authentication provider, or signing them into
|
users the choice of more than one authentication provider, or signing them into
|
||||||
multiple services simultaneously) then you will want to create a resource with
|
multiple services simultaneously) then you will want to create a resource with
|
||||||
this extension enabled. It is used to keep track of the links between your
|
this extension enabled. It is used to keep track of the links between your
|
||||||
local user records and their many remote identities.
|
local user records and their many remote identities.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
Let's create an `Accounts` API in our application which provides a `User`
|
Let's create an `Accounts` domain in our application which provides a `User`
|
||||||
resource and a `Token` resource.
|
resource and a `Token` resource.
|
||||||
|
|
||||||
First, let's define our API:
|
First, let's define our domain:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# lib/my_app/accounts.ex
|
# lib/my_app/accounts.ex
|
||||||
|
|
||||||
defmodule MyApp.Accounts do
|
defmodule MyApp.Accounts do
|
||||||
use Ash.Api
|
use Ash.Domain
|
||||||
|
|
||||||
resources do
|
resources do
|
||||||
resource MyApp.Accounts.User
|
resource MyApp.Accounts.User
|
||||||
|
@ -105,15 +105,15 @@ defmodule MyApp.Accounts do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
Be sure to add it to the `ash_apis` config in your `config.exs`
|
Be sure to add it to the `ash_domains` config in your `config.exs`
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# in config/config.exs
|
# in config/config.exs
|
||||||
config :my_app, :ash_apis: [..., MyApp.Accounts]
|
config :my_app, :ash_domains: [..., MyApp.Accounts]
|
||||||
```
|
```
|
||||||
|
|
||||||
Next, let's define our `Token` resource. This resource is needed
|
Next, let's define our `Token` resource. This resource is needed
|
||||||
if token generation is enabled for any resources in your application. Most of
|
if token generation is enabled for any resources in your application. Most of
|
||||||
the contents are auto-generated, so we just need to provide the data layer
|
the contents are auto-generated, so we just need to provide the data layer
|
||||||
configuration and the API to use.
|
configuration and the API to use.
|
||||||
|
|
||||||
|
@ -140,11 +140,8 @@ You can skip this step if you don't want to use tokens, in which case remove the
|
||||||
defmodule MyApp.Accounts.Token do
|
defmodule MyApp.Accounts.Token do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.TokenResource]
|
extensions: [AshAuthentication.TokenResource],
|
||||||
|
domain: MyApp.Accounts
|
||||||
token do
|
|
||||||
api MyApp.Accounts
|
|
||||||
end
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "tokens"
|
table "tokens"
|
||||||
|
@ -170,17 +167,16 @@ defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication],
|
extensions: [AshAuthentication],
|
||||||
authorizers: [Ash.Policy.Authorizer]
|
authorizers: [Ash.Policy.Authorizer],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
attribute :email, :ci_string, allow_nil?: false
|
attribute :email, :ci_string, allow_nil?: false, public?: true
|
||||||
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true, private?: true
|
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
@ -231,8 +227,8 @@ If you're using Phoenix, then you can skip this section and go straight to
|
||||||
[Integrating Ash Authentication and Phoenix](https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix)
|
[Integrating Ash Authentication and Phoenix](https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix)
|
||||||
|
|
||||||
In order for your users to be able to sign in, you will likely need to provide
|
In order for your users to be able to sign in, you will likely need to provide
|
||||||
an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication
|
an HTTP endpoint to submit credentials or OAuth requests to. Ash Authentication
|
||||||
provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro
|
provides `AshAuthentication.Plug` for this purposes. It provides a `use` macro
|
||||||
which handles routing of requests to the correct providers, and defines
|
which handles routing of requests to the correct providers, and defines
|
||||||
callbacks for successful and unsuccessful outcomes.
|
callbacks for successful and unsuccessful outcomes.
|
||||||
|
|
||||||
|
@ -290,7 +286,7 @@ based on the contents of the session store or `Authorization` header.
|
||||||
## Supervisor
|
## Supervisor
|
||||||
|
|
||||||
AshAuthentication includes a supervisor which you should add to your
|
AshAuthentication includes a supervisor which you should add to your
|
||||||
application's supervisor tree. This is used to run any periodic jobs related to
|
application's supervisor tree. This is used to run any periodic jobs related to
|
||||||
your authenticated resources (removing expired tokens, for example).
|
your authenticated resources (removing expired tokens, for example).
|
||||||
|
|
||||||
### Example
|
### Example
|
||||||
|
@ -314,9 +310,9 @@ end
|
||||||
## Token generation
|
## Token generation
|
||||||
|
|
||||||
If you have token generation enabled then you need to provide (at minimum) a
|
If you have token generation enabled then you need to provide (at minimum) a
|
||||||
signing secret. As the name implies this should be a secret. AshAuthentication
|
signing secret. As the name implies this should be a secret. AshAuthentication
|
||||||
provides a mechanism for looking up secrets at runtime using the
|
provides a mechanism for looking up secrets at runtime using the
|
||||||
`AshAuthentication.Secret` behaviour. To save you a click, this means that you
|
`AshAuthentication.Secret` behaviour. To save you a click, this means that you
|
||||||
can set your token signing secret using either a static string (please don't!),
|
can set your token signing secret using either a static string (please don't!),
|
||||||
a two-arity anonymous function, or a module which implements the
|
a two-arity anonymous function, or a module which implements the
|
||||||
`AshAuthentication.Secret` behaviour.
|
`AshAuthentication.Secret` behaviour.
|
||||||
|
|
|
@ -6,37 +6,41 @@ GitHub for authentication.
|
||||||
First you need to configure an application in your [GitHub developer
|
First you need to configure an application in your [GitHub developer
|
||||||
settings](https://github.com/settings/developers):
|
settings](https://github.com/settings/developers):
|
||||||
|
|
||||||
1. Click the "New OAuth App" button.
|
1. Click the "New OAuth App" button.
|
||||||
2. Set your application name to something that identifies it. You will likely
|
2. Set your application name to something that identifies it. You will likely
|
||||||
need separate applications for development and production environments, so
|
need separate applications for development and production environments, so
|
||||||
keep that in mind.
|
keep that in mind.
|
||||||
3. Set "Homepage URL" appropriately for your application and environment.
|
3. Set "Homepage URL" appropriately for your application and environment.
|
||||||
4. In the "Authorization callback URL" section, add your callback URL. The
|
4. In the "Authorization callback URL" section, add your callback URL. The
|
||||||
callback URL is generated from the following information:
|
callback URL is generated from the following information:
|
||||||
- The base URL of the application - in development that would be
|
|
||||||
`http://localhost:4000/` but in production will be your application's
|
|
||||||
URL.
|
|
||||||
- The mount point of the auth routes in your router - we'll assume
|
|
||||||
`/auth`.
|
|
||||||
- The "subject name" of the resource being authenticated - we'll assume `user`.
|
|
||||||
- The name of the strategy in your configuration. By default this is
|
|
||||||
`github`.
|
|
||||||
|
|
||||||
This means that the callback URL should look something like
|
- The base URL of the application - in development that would be
|
||||||
`http://localhost:4000/auth/user/github/callback`.
|
`http://localhost:4000/` but in production will be your application's
|
||||||
5. Do not set "Enable Device Flow" unless you know why you want this.
|
URL.
|
||||||
6. Click "Register application".
|
- The mount point of the auth routes in your router - we'll assume
|
||||||
7. Click "Generate a new client secret".
|
`/auth`.
|
||||||
8. Copy the "Client ID" and "Client secret" somewhere safe, we'll need them
|
- The "subject name" of the resource being authenticated - we'll assume `user`.
|
||||||
soon.
|
- The name of the strategy in your configuration. By default this is
|
||||||
9. Click "Update application".
|
`github`.
|
||||||
|
|
||||||
|
This means that the callback URL should look something like
|
||||||
|
`http://localhost:4000/auth/user/github/callback`.
|
||||||
|
|
||||||
|
5. Do not set "Enable Device Flow" unless you know why you want this.
|
||||||
|
6. Click "Register application".
|
||||||
|
7. Click "Generate a new client secret".
|
||||||
|
8. Copy the "Client ID" and "Client secret" somewhere safe, we'll need them
|
||||||
|
soon.
|
||||||
|
9. Click "Update application".
|
||||||
|
|
||||||
Next we can configure our resource (assuming you already have everything else
|
Next we can configure our resource (assuming you already have everything else
|
||||||
set up):
|
set up):
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
strategies do
|
strategies do
|
||||||
|
@ -82,28 +86,31 @@ end
|
||||||
|
|
||||||
The values for this configuration should be:
|
The values for this configuration should be:
|
||||||
|
|
||||||
* `client_id` - the client ID copied from the GitHub settings page.
|
- `client_id` - the client ID copied from the GitHub settings page.
|
||||||
* `redirect_uri` - the URL to the generated auth routes in your application
|
- `redirect_uri` - the URL to the generated auth routes in your application
|
||||||
(eg `http://localhost:4000/auth`).
|
(eg `http://localhost:4000/auth`).
|
||||||
* `client_secret` the client secret copied from the GitHub settings page.
|
- `client_secret` the client secret copied from the GitHub settings page.
|
||||||
|
|
||||||
Lastly, we need to add a register action to your user resource. This is defined
|
Lastly, we need to add a register action to your user resource. This is defined
|
||||||
as an upsert so that it can register new users, or update information for
|
as an upsert so that it can register new users, or update information for
|
||||||
returning users. The default name of the action is `register_with_` followed by
|
returning users. The default name of the action is `register_with_` followed by
|
||||||
the strategy name. In our case that is `register_with_github`.
|
the strategy name. In our case that is `register_with_github`.
|
||||||
|
|
||||||
The register action takes two arguments, `user_info` and the `oauth_tokens`.
|
The register action takes two arguments, `user_info` and the `oauth_tokens`.
|
||||||
- `user_info` contains the [`GET /user` response from
|
|
||||||
GitHub](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user)
|
- `user_info` contains the [`GET /user` response from
|
||||||
which you can use to populate your user attributes as needed.
|
GitHub](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user)
|
||||||
- `oauth_tokens` contains the [`POST /login/oauth/access_token` response from
|
which you can use to populate your user attributes as needed.
|
||||||
GitHub](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response)
|
- `oauth_tokens` contains the [`POST /login/oauth/access_token` response from
|
||||||
- you may want to store these if you intend to call the GitHub API on behalf
|
GitHub](https://docs.github.com/en/developers/apps/building-oauth-apps/authorizing-oauth-apps#response)
|
||||||
|
- you may want to store these if you intend to call the GitHub API on behalf
|
||||||
of the user.
|
of the user.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,11 @@ First you'll need a registered application in [Google Cloud](https://console.clo
|
||||||
|
|
||||||
Next we configure our resource to use google credentials:
|
Next we configure our resource to use google credentials:
|
||||||
|
|
||||||
``` elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
...
|
...
|
||||||
|
@ -34,9 +36,12 @@ end
|
||||||
Please check the guide on how to properly configure your Secrets
|
Please check the guide on how to properly configure your Secrets
|
||||||
Then we need to define an action that will handle the oauth2 flow, for the google case it is `:register_with_google` it will handle both cases for our resource, user registration & login.
|
Then we need to define an action that will handle the oauth2 flow, for the google case it is `:register_with_google` it will handle both cases for our resource, user registration & login.
|
||||||
|
|
||||||
``` elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
actions do
|
actions do
|
||||||
create :register_with_google do
|
create :register_with_google do
|
||||||
|
|
|
@ -16,7 +16,8 @@ defmodule AshAuthentication do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -25,8 +26,6 @@ defmodule AshAuthentication do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
@ -83,7 +82,7 @@ defmodule AshAuthentication do
|
||||||
for more information.
|
for more information.
|
||||||
"""
|
"""
|
||||||
alias Ash.{
|
alias Ash.{
|
||||||
Api,
|
Domain,
|
||||||
Error.Query.NotFound,
|
Error.Query.NotFound,
|
||||||
Query,
|
Query,
|
||||||
Resource
|
Resource
|
||||||
|
@ -120,7 +119,7 @@ defmodule AshAuthentication do
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
|
||||||
@type resource_config :: %{
|
@type resource_config :: %{
|
||||||
api: module,
|
domain: module,
|
||||||
providers: [module],
|
providers: [module],
|
||||||
resource: module,
|
resource: module,
|
||||||
subject_name: atom
|
subject_name: atom
|
||||||
|
@ -145,8 +144,8 @@ defmodule AshAuthentication do
|
||||||
|> List.wrap()
|
|> List.wrap()
|
||||||
|> Enum.flat_map(fn otp_app ->
|
|> Enum.flat_map(fn otp_app ->
|
||||||
otp_app
|
otp_app
|
||||||
|> Application.get_env(:ash_apis, [])
|
|> Application.get_env(:ash_domains, [])
|
||||||
|> Stream.flat_map(&Api.Info.resources(&1))
|
|> Stream.flat_map(&Domain.Info.resources(&1))
|
||||||
|> Stream.uniq()
|
|> Stream.uniq()
|
||||||
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|
||||||
|> Enum.to_list()
|
|> Enum.to_list()
|
||||||
|
@ -185,7 +184,7 @@ defmodule AshAuthentication do
|
||||||
iex> %{id: user_id} = build_user()
|
iex> %{id: user_id} = build_user()
|
||||||
...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User)
|
...> {:ok, %{id: ^user_id}} = subject_to_user("user?id=#{user_id}", Example.User)
|
||||||
|
|
||||||
Any options passed will be passed to the underlying `Api.read/2` callback.
|
Any options passed will be passed to the underlying `Domain.read/2` callback.
|
||||||
"""
|
"""
|
||||||
@spec subject_to_user(subject | URI.t(), Resource.t(), keyword) ::
|
@spec subject_to_user(subject | URI.t(), Resource.t(), keyword) ::
|
||||||
{:ok, Resource.record()} | {:error, any}
|
{:ok, Resource.record()} | {:error, any}
|
||||||
|
@ -199,7 +198,7 @@ defmodule AshAuthentication do
|
||||||
with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource),
|
with {:ok, resource_subject_name} <- Info.authentication_subject_name(resource),
|
||||||
^subject_name <- to_string(resource_subject_name),
|
^subject_name <- to_string(resource_subject_name),
|
||||||
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource),
|
{:ok, action_name} <- Info.authentication_get_by_subject_action_name(resource),
|
||||||
{:ok, api} <- Info.authentication_api(resource) do
|
{:ok, domain} <- Info.domain(resource) do
|
||||||
primary_key =
|
primary_key =
|
||||||
primary_key
|
primary_key
|
||||||
|> URI.decode_query()
|
|> URI.decode_query()
|
||||||
|
@ -214,7 +213,7 @@ defmodule AshAuthentication do
|
||||||
})
|
})
|
||||||
|> Query.for_read(action_name, %{})
|
|> Query.for_read(action_name, %{})
|
||||||
|> Query.filter(^primary_key)
|
|> Query.filter(^primary_key)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} -> {:ok, user}
|
{:ok, [user]} -> {:ok, user}
|
||||||
_ -> {:error, NotFound.exception([])}
|
_ -> {:error, NotFound.exception([])}
|
||||||
|
|
|
@ -21,7 +21,8 @@ defmodule AshAuthentication.AddOn.Confirmation do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -29,8 +30,6 @@ defmodule AshAuthentication.AddOn.Confirmation do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
add_ons do
|
add_ons do
|
||||||
confirmation :confirm do
|
confirmation :confirm do
|
||||||
monitor_fields [:email]
|
monitor_fields [:email]
|
||||||
|
|
|
@ -21,7 +21,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
"""
|
"""
|
||||||
@spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
@spec confirm(Confirmation.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
||||||
def confirm(strategy, params, opts \\ []) do
|
def confirm(strategy, params, opts \\ []) do
|
||||||
with {:ok, api} <- Info.authentication_api(strategy.resource),
|
with {:ok, domain} <- Info.domain(strategy.resource),
|
||||||
{:ok, token} <- Map.fetch(params, "confirm"),
|
{:ok, token} <- Map.fetch(params, "confirm"),
|
||||||
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
|
{:ok, %{"sub" => subject}, _} <- Jwt.verify(token, strategy.resource),
|
||||||
{:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts) do
|
{:ok, user} <- AshAuthentication.subject_to_user(subject, strategy.resource, opts) do
|
||||||
|
@ -33,7 +33,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Changeset.for_update(strategy.confirm_action_name, params)
|
|> Changeset.for_update(strategy.confirm_action_name, params)
|
||||||
|> api.update(opts)
|
|> domain.update(opts)
|
||||||
else
|
else
|
||||||
:error -> {:error, InvalidToken.exception(type: :confirmation)}
|
:error -> {:error, InvalidToken.exception(type: :confirmation)}
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -52,7 +52,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
|> Map.new()
|
|> Map.new()
|
||||||
|
|
||||||
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
|
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
|
||||||
{:ok, api} <- TokenResource.Info.token_api(token_resource),
|
{:ok, domain} <- TokenResource.Info.token_domain(token_resource),
|
||||||
{:ok, store_changes_action} <-
|
{:ok, store_changes_action} <-
|
||||||
TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
|
TokenResource.Info.token_confirmation_store_changes_action_name(token_resource),
|
||||||
{:ok, _token_record} <-
|
{:ok, _token_record} <-
|
||||||
|
@ -68,7 +68,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
extra_data: changes,
|
extra_data: changes,
|
||||||
purpose: to_string(Strategy.name(strategy))
|
purpose: to_string(Strategy.name(strategy))
|
||||||
})
|
})
|
||||||
|> api.create(Keyword.merge(opts, upsert?: true)) do
|
|> domain.create(Keyword.merge(opts, upsert?: true)) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
|
@ -88,7 +88,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
@spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
|
@spec get_changes(Confirmation.t(), String.t(), keyword) :: {:ok, map} | :error
|
||||||
def get_changes(strategy, jti, opts \\ []) do
|
def get_changes(strategy, jti, opts \\ []) do
|
||||||
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
|
with {:ok, token_resource} <- Info.authentication_tokens_token_resource(strategy.resource),
|
||||||
{:ok, api} <- TokenResource.Info.token_api(token_resource),
|
{:ok, domain} <- TokenResource.Info.token_domain(token_resource),
|
||||||
{:ok, get_changes_action} <-
|
{:ok, get_changes_action} <-
|
||||||
TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
|
TokenResource.Info.token_confirmation_get_changes_action_name(token_resource),
|
||||||
{:ok, [token_record]} <-
|
{:ok, [token_record]} <-
|
||||||
|
@ -101,7 +101,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Actions do
|
||||||
})
|
})
|
||||||
|> Query.set_context(%{strategy: strategy})
|
|> Query.set_context(%{strategy: strategy})
|
||||||
|> Query.for_read(get_changes_action, %{"jti" => jti})
|
|> Query.for_read(get_changes_action, %{"jti" => jti})
|
||||||
|> api.read(opts) do
|
|> domain.read(opts) do
|
||||||
changes =
|
changes =
|
||||||
strategy.monitor_fields
|
strategy.monitor_fields
|
||||||
|> Stream.map(&to_string/1)
|
|> Stream.map(&to_string/1)
|
||||||
|
|
|
@ -29,6 +29,11 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
|
||||||
|
|
||||||
defp do_change(changeset, strategy) do
|
defp do_change(changeset, strategy) do
|
||||||
changeset
|
changeset
|
||||||
|
|> Changeset.set_context(%{
|
||||||
|
private: %{
|
||||||
|
ash_authentication?: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|> Changeset.before_action(fn changeset ->
|
|> Changeset.before_action(fn changeset ->
|
||||||
with token when is_binary(token) <- Changeset.get_argument(changeset, :confirm),
|
with token when is_binary(token) <- Changeset.get_argument(changeset, :confirm),
|
||||||
{:ok, %{"act" => action, "jti" => jti}, _} <-
|
{:ok, %{"act" => action, "jti" => jti}, _} <-
|
||||||
|
@ -41,11 +46,14 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmChange do
|
||||||
else: %{}
|
else: %{}
|
||||||
|
|
||||||
changeset
|
changeset
|
||||||
|> Changeset.change_attributes(allowed_changes)
|
|> Changeset.force_change_attributes(allowed_changes)
|
||||||
|> Changeset.change_attribute(strategy.confirmed_at_field, DateTime.utc_now())
|
|> Changeset.change_attribute(strategy.confirmed_at_field, DateTime.utc_now())
|
||||||
else
|
else
|
||||||
_ ->
|
_ ->
|
||||||
raise InvalidArgument, field: :confirm, message: "is not valid"
|
changeset
|
||||||
|
|> Changeset.add_error(
|
||||||
|
InvalidArgument.exception(field: :confirm, message: "is not valid")
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,15 +53,18 @@ defmodule AshAuthentication.AddOn.Confirmation.ConfirmationHookChange do
|
||||||
|
|
||||||
defp do_change(changeset, strategy) do
|
defp do_change(changeset, strategy) do
|
||||||
changeset
|
changeset
|
||||||
|> Changeset.before_action(fn changeset ->
|
|> Changeset.before_action(
|
||||||
changeset
|
fn changeset ->
|
||||||
|> not_confirm_action(strategy)
|
changeset
|
||||||
|> should_confirm_action_type(strategy)
|
|> not_confirm_action(strategy)
|
||||||
|> monitored_field_changing(strategy)
|
|> should_confirm_action_type(strategy)
|
||||||
|> changes_would_be_valid()
|
|> monitored_field_changing(strategy)
|
||||||
|> maybe_inhibit_updates(strategy)
|
|> changes_would_be_valid()
|
||||||
|> maybe_perform_confirmation(strategy, changeset)
|
|> maybe_inhibit_updates(strategy)
|
||||||
end)
|
|> maybe_perform_confirmation(strategy, changeset)
|
||||||
|
end,
|
||||||
|
prepend?: true
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp not_confirm_action(%Changeset{} = changeset, strategy)
|
defp not_confirm_action(%Changeset{} = changeset, strategy)
|
||||||
|
|
|
@ -83,6 +83,7 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
|
||||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, field),
|
{:ok, attribute} <- find_attribute(dsl_state, field),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
||||||
|
:ok <- validate_attribute_option(attribute, resource, :public?, [true]),
|
||||||
:ok <- maybe_validate_eager_checking(dsl_state, strategy, field, resource) do
|
:ok <- maybe_validate_eager_checking(dsl_state, strategy, field, resource) do
|
||||||
{:cont, :ok}
|
{:cont, :ok}
|
||||||
else
|
else
|
||||||
|
@ -142,7 +143,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
|
||||||
accept: strategy.monitor_fields,
|
accept: strategy.monitor_fields,
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
changes: changes
|
changes: changes,
|
||||||
|
require_atomic?: false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -150,8 +152,26 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do
|
||||||
with {:ok, action} <- validate_action_exists(dsl_state, strategy.confirm_action_name),
|
with {:ok, action} <- validate_action_exists(dsl_state, strategy.confirm_action_name),
|
||||||
:ok <- validate_action_has_change(action, Confirmation.ConfirmChange),
|
:ok <- validate_action_has_change(action, Confirmation.ConfirmChange),
|
||||||
:ok <- validate_action_argument_option(action, :confirm, :allow_nil?, [false]),
|
:ok <- validate_action_argument_option(action, :confirm, :allow_nil?, [false]),
|
||||||
:ok <- validate_action_has_change(action, GenerateTokenChange) do
|
:ok <- validate_action_argument_option(action, :confirm, :type, [Type.String]),
|
||||||
validate_action_argument_option(action, :confirm, :type, [Type.String])
|
:ok <- validate_action_has_change(action, GenerateTokenChange),
|
||||||
|
:ok <- validate_action_option(action, :require_atomic?, [false]) do
|
||||||
|
accept_fields = MapSet.new(action.accept)
|
||||||
|
|
||||||
|
strategy.monitor_fields
|
||||||
|
|> MapSet.new()
|
||||||
|
|> MapSet.difference(accept_fields)
|
||||||
|
|> Enum.to_list()
|
||||||
|
|> case do
|
||||||
|
[] ->
|
||||||
|
:ok
|
||||||
|
|
||||||
|
_fields ->
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
path: [:actions, action.name, :accept],
|
||||||
|
message: "The confirmation action must accept the monitored fields."
|
||||||
|
)}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ defmodule AshAuthentication.Dsl do
|
||||||
import AshAuthentication.Utils, only: [to_sentence: 2]
|
import AshAuthentication.Utils, only: [to_sentence: 2]
|
||||||
import Joken.Signer, only: [algorithms: 0]
|
import Joken.Signer, only: [algorithms: 0]
|
||||||
|
|
||||||
alias Ash.{Api, Resource}
|
alias Ash.{Domain, Resource}
|
||||||
|
|
||||||
@default_token_lifetime_days 14
|
@default_token_lifetime_days 14
|
||||||
|
|
||||||
|
@ -41,17 +41,18 @@ defmodule AshAuthentication.Dsl do
|
||||||
%Section{
|
%Section{
|
||||||
name: :authentication,
|
name: :authentication,
|
||||||
describe: "Configure authentication for this resource",
|
describe: "Configure authentication for this resource",
|
||||||
modules: [:api],
|
modules: [:domain],
|
||||||
schema: [
|
schema: [
|
||||||
subject_name: [
|
subject_name: [
|
||||||
type: :atom,
|
type: :atom,
|
||||||
doc:
|
doc:
|
||||||
"The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`)."
|
"The subject name is used anywhere that a short version of your resource name is needed. Must be unique system-wide and will be inferred from the resource name by default (ie `MyApp.Accounts.User` -> `user`)."
|
||||||
],
|
],
|
||||||
api: [
|
domain: [
|
||||||
type: {:behaviour, Api},
|
type: {:behaviour, Domain},
|
||||||
|
required: false,
|
||||||
doc:
|
doc:
|
||||||
"The name of the Ash API to use to access this resource when doing anything authenticaiton related."
|
"The name of the Ash domain to use to access this resource when doing anything authentication related."
|
||||||
],
|
],
|
||||||
get_by_subject_action_name: [
|
get_by_subject_action_name: [
|
||||||
type: :atom,
|
type: :atom,
|
||||||
|
|
|
@ -3,16 +3,20 @@ defmodule AshAuthentication.Errors.AuthenticationFailed do
|
||||||
A generic, authentication failed error.
|
A generic, authentication failed error.
|
||||||
"""
|
"""
|
||||||
use Ash.Error.Exception
|
use Ash.Error.Exception
|
||||||
def_ash_error([:field, :strategy, caused_by: %{}], class: :forbidden)
|
|
||||||
import AshAuthentication.Debug
|
use Splode.Error,
|
||||||
|
fields: [
|
||||||
|
caused_by: %{},
|
||||||
|
changeset: nil,
|
||||||
|
field: nil,
|
||||||
|
query: nil,
|
||||||
|
strategy: nil
|
||||||
|
],
|
||||||
|
class: :forbidden
|
||||||
|
|
||||||
@type t :: Exception.t()
|
@type t :: Exception.t()
|
||||||
|
|
||||||
def exception(args) do
|
def message(_), do: "Authentication failed"
|
||||||
args
|
|
||||||
|> super()
|
|
||||||
|> describe()
|
|
||||||
end
|
|
||||||
|
|
||||||
defimpl Ash.ErrorKind do
|
defimpl Ash.ErrorKind do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
|
@ -3,7 +3,9 @@ defmodule AshAuthentication.Errors.InvalidToken do
|
||||||
An invalid token was presented.
|
An invalid token was presented.
|
||||||
"""
|
"""
|
||||||
use Ash.Error.Exception
|
use Ash.Error.Exception
|
||||||
def_ash_error([:type], class: :forbidden)
|
use Splode.Error, fields: [:type], class: :forbidden
|
||||||
|
|
||||||
|
def message(%{type: type}), do: "Invalid #{type} token"
|
||||||
|
|
||||||
defimpl Ash.ErrorKind do
|
defimpl Ash.ErrorKind do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
|
@ -3,7 +3,11 @@ defmodule AshAuthentication.Errors.MissingSecret do
|
||||||
A secret is now missing.
|
A secret is now missing.
|
||||||
"""
|
"""
|
||||||
use Ash.Error.Exception
|
use Ash.Error.Exception
|
||||||
def_ash_error([:resource], class: :forbidden)
|
use Splode.Error, fields: [:resource], class: :forbidden
|
||||||
|
|
||||||
|
def message(%{path: path, resource: resource}) do
|
||||||
|
"Secret for `#{Enum.join(path, ".")}` on the `#{inspect(resource)}` resource is not accessible."
|
||||||
|
end
|
||||||
|
|
||||||
defimpl Ash.ErrorKind do
|
defimpl Ash.ErrorKind do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
|
|
@ -7,7 +7,7 @@ defmodule AshAuthentication.Info do
|
||||||
extension: AshAuthentication,
|
extension: AshAuthentication,
|
||||||
sections: [:authentication]
|
sections: [:authentication]
|
||||||
|
|
||||||
alias Ash.{Changeset, Query}
|
alias Ash.{Changeset, Domain, Query, Resource}
|
||||||
alias AshAuthentication.Strategy
|
alias AshAuthentication.Strategy
|
||||||
alias Spark.Dsl.Extension
|
alias Spark.Dsl.Extension
|
||||||
|
|
||||||
|
@ -103,4 +103,33 @@ defmodule AshAuthentication.Info do
|
||||||
{:ok, strategy}
|
{:ok, strategy}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Retrieve the domain to use for authentication.
|
||||||
|
|
||||||
|
If the `authentication.domain` DSL option is set, it will be used, otherwise
|
||||||
|
it will default to that configured on the resource.
|
||||||
|
"""
|
||||||
|
@spec domain(dsl_or_resource) :: {:ok, Domain.t()} | :error
|
||||||
|
def domain(dsl_or_resource) do
|
||||||
|
auth_domain =
|
||||||
|
case authentication_domain(dsl_or_resource) do
|
||||||
|
{:ok, value} -> value
|
||||||
|
:error -> nil
|
||||||
|
end
|
||||||
|
|
||||||
|
resource_domain = Resource.Info.domain(dsl_or_resource)
|
||||||
|
|
||||||
|
domain = auth_domain || resource_domain
|
||||||
|
|
||||||
|
if domain, do: {:ok, domain}, else: :error
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc "Raising version of `domain/1`"
|
||||||
|
def domain!(dsl_or_resource) do
|
||||||
|
case domain(dsl_or_resource) do
|
||||||
|
{:ok, value} -> value
|
||||||
|
:error -> raise "No `domain` configured on resource `#{inspect(dsl_or_resource)}`"
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule AshAuthentication.Plug.Macros do
|
||||||
Generators used within `use AshAuthentication.Plug`.
|
Generators used within `use AshAuthentication.Plug`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
alias Ash.Api
|
alias Ash.Domain
|
||||||
alias AshAuthentication.Plug.Helpers
|
alias AshAuthentication.Plug.Helpers
|
||||||
alias Plug.Conn
|
alias Plug.Conn
|
||||||
alias Spark.Dsl.Extension
|
alias Spark.Dsl.Extension
|
||||||
|
@ -14,11 +14,9 @@ defmodule AshAuthentication.Plug.Macros do
|
||||||
@spec validate_subject_name_uniqueness(atom) :: Macro.t()
|
@spec validate_subject_name_uniqueness(atom) :: Macro.t()
|
||||||
defmacro validate_subject_name_uniqueness(otp_app) do
|
defmacro validate_subject_name_uniqueness(otp_app) do
|
||||||
quote do
|
quote do
|
||||||
require Ash.Api.Info
|
|
||||||
|
|
||||||
unquote(otp_app)
|
unquote(otp_app)
|
||||||
|> Application.compile_env(:ash_apis, [])
|
|> Application.compile_env(:ash_domains, [])
|
||||||
|> Stream.flat_map(&Api.Info.depend_on_resources(&1))
|
|> Stream.flat_map(&Domain.Info.resources(&1))
|
||||||
|> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)})
|
|> Stream.map(&{&1, Extension.get_persisted(&1, :authentication)})
|
||||||
|> Stream.reject(&(elem(&1, 1) == nil))
|
|> Stream.reject(&(elem(&1, 1) == nil))
|
||||||
|> Stream.map(&{elem(&1, 0), elem(&1, 1).subject_name})
|
|> Stream.map(&{elem(&1, 0), elem(&1, 1).subject_name})
|
||||||
|
|
|
@ -22,15 +22,14 @@ defmodule AshAuthentication.Plug.Router do
|
||||||
|> Macro.expand_once(__CALLER__)
|
|> Macro.expand_once(__CALLER__)
|
||||||
|
|
||||||
quote do
|
quote do
|
||||||
require Ash.Api.Info
|
|
||||||
use Plug.Router
|
use Plug.Router
|
||||||
plug(:match)
|
plug(:match)
|
||||||
plug(:dispatch)
|
plug(:dispatch)
|
||||||
|
|
||||||
routes =
|
routes =
|
||||||
unquote(otp_app)
|
unquote(otp_app)
|
||||||
|> Application.compile_env(:ash_apis, [])
|
|> Application.compile_env(:ash_domains, [])
|
||||||
|> Stream.flat_map(&Ash.Api.Info.depend_on_resources(&1))
|
|> Stream.flat_map(&Ash.Domain.Info.resources(&1))
|
||||||
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|
|> Stream.filter(&(AshAuthentication in Spark.extensions(&1)))
|
||||||
|> Stream.flat_map(&Info.authentication_strategies/1)
|
|> Stream.flat_map(&Info.authentication_strategies/1)
|
||||||
|> Stream.flat_map(fn strategy ->
|
|> Stream.flat_map(fn strategy ->
|
||||||
|
|
|
@ -15,11 +15,11 @@ defmodule AshAuthentication.Secret do
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 do
|
oauth2 do
|
||||||
client_id MyApp.GetSecret
|
client_id MyApp.GetSecret
|
||||||
|
@ -34,11 +34,11 @@ defmodule AshAuthentication.Secret do
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.User do
|
defmodule MyApp.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 do
|
oauth2 do
|
||||||
client_id fn _secret, _resource ->
|
client_id fn _secret, _resource ->
|
||||||
|
|
|
@ -41,11 +41,11 @@ defmodule AshAuthentication.Sender do
|
||||||
end
|
end
|
||||||
|
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
resettable do
|
resettable do
|
||||||
|
@ -57,15 +57,15 @@ defmodule AshAuthentication.Sender do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also implment it directly as a function:
|
You can also implement it directly as a function:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
resettable do
|
resettable do
|
||||||
|
|
|
@ -18,7 +18,8 @@ defmodule AshAuthentication.Strategy.MagicLink do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -26,8 +27,6 @@ defmodule AshAuthentication.Strategy.MagicLink do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
magic_link do
|
magic_link do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
|
|
@ -14,13 +14,13 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
|
||||||
"""
|
"""
|
||||||
@spec request(MagicLink.t(), map, keyword) :: :ok | {:error, any}
|
@spec request(MagicLink.t(), map, keyword) :: :ok | {:error, any}
|
||||||
def request(strategy, params, options) do
|
def request(strategy, params, options) do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
||||||
|> Query.for_read(strategy.request_action_name, params)
|
|> Query.for_read(strategy.request_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -33,13 +33,13 @@ defmodule AshAuthentication.Strategy.MagicLink.Actions do
|
||||||
@spec sign_in(MagicLink.t(), map, keyword) ::
|
@spec sign_in(MagicLink.t(), map, keyword) ::
|
||||||
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
|
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
|
||||||
def sign_in(strategy, params, options) do
|
def sign_in(strategy, params, options) do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
||||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
|
|
@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _opts, _context) do
|
def prepare(query, _opts, _context) do
|
||||||
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ defmodule AshAuthentication.Strategy.MagicLink.RequestPreparation do
|
||||||
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(^ref(identity_field) == ^identity)
|
||||||
|> Query.before_action(fn query ->
|
|> Query.before_action(fn query ->
|
||||||
Ash.Query.ensure_selected(query, select_for_senders)
|
Ash.Query.ensure_selected(query, select_for_senders)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule AshAuthentication.Strategy.MagicLink.SignInPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _otps, _context) do
|
def prepare(query, _otps, _context) do
|
||||||
subject_name =
|
subject_name =
|
||||||
query.resource
|
query.resource
|
||||||
|
|
|
@ -19,7 +19,8 @@ defmodule AshAuthentication.Strategy.OAuth2 do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -27,8 +28,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
client_id "OAuth Client ID"
|
client_id "OAuth Client ID"
|
||||||
|
@ -157,8 +156,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
registration_enabled? false
|
registration_enabled? false
|
||||||
|
@ -195,8 +192,6 @@ defmodule AshAuthentication.Strategy.OAuth2 do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
oauth2 :example do
|
oauth2 :example do
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,7 +22,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
||||||
)}
|
)}
|
||||||
|
|
||||||
def sign_in(%OAuth2{} = strategy, params, options) do
|
def sign_in(%OAuth2{} = strategy, params, options) do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|
@ -32,7 +32,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
@ -90,7 +90,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
||||||
"""
|
"""
|
||||||
@spec register(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
@spec register(OAuth2.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
||||||
def register(%OAuth2{} = strategy, params, options) when strategy.registration_enabled? do
|
def register(%OAuth2{} = strategy, params, options) when strategy.registration_enabled? do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create)
|
action = Resource.Info.action(strategy.resource, strategy.register_action_name, :create)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|
@ -104,7 +104,7 @@ defmodule AshAuthentication.Strategy.OAuth2.Actions do
|
||||||
upsert?: true,
|
upsert?: true,
|
||||||
upsert_identity: action.upsert_identity
|
upsert_identity: action.upsert_identity
|
||||||
)
|
)
|
||||||
|> api.create(options)
|
|> domain.create(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def register(%OAuth2{} = strategy, _params, _options),
|
def register(%OAuth2{} = strategy, _params, _options),
|
||||||
|
|
|
@ -42,7 +42,7 @@ defmodule AshAuthentication.Strategy.OAuth2.IdentityChange do
|
||||||
"#{user_id_attribute_name}": user.id
|
"#{user_id_attribute_name}": user.id
|
||||||
}) do
|
}) do
|
||||||
user
|
user
|
||||||
|> changeset.api.load(strategy.identity_relationship_name)
|
|> changeset.domain.load(strategy.identity_relationship_name)
|
||||||
else
|
else
|
||||||
:error -> :error
|
:error -> :error
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
|
|
@ -17,7 +17,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _opts, _context) do
|
def prepare(query, _opts, _context) do
|
||||||
case Info.strategy_for_action(query.resource, query.action.name) do
|
case Info.strategy_for_action(query.resource, query.action.name) do
|
||||||
:error ->
|
:error ->
|
||||||
|
@ -70,7 +70,7 @@ defmodule AshAuthentication.Strategy.OAuth2.SignInPreparation do
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _identity} ->
|
{:ok, _identity} ->
|
||||||
user
|
user
|
||||||
|> query.api.load(strategy.identity_relationship_name)
|
|> query.domain.load(strategy.identity_relationship_name)
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
|
|
|
@ -18,7 +18,8 @@ defmodule AshAuthentication.Strategy.Password do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.User do
|
defmodule MyApp.Accounts.User do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication]
|
extensions: [AshAuthentication],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
@ -27,8 +28,6 @@ defmodule AshAuthentication.Strategy.Password do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api MyApp.Accounts
|
|
||||||
|
|
||||||
strategies do
|
strategies do
|
||||||
password :password do
|
password :password do
|
||||||
identity_field :email
|
identity_field :email
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
|
{:ok, Resource.record()} | {:error, Errors.AuthenticationFailed.t()}
|
||||||
def sign_in(strategy, params, options)
|
def sign_in(strategy, params, options)
|
||||||
when is_struct(strategy, Password) and strategy.sign_in_enabled? do
|
when is_struct(strategy, Password) and strategy.sign_in_enabled? do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
{context, options} = Keyword.pop(options, :context, [])
|
{context, options} = Keyword.pop(options, :context, [])
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|> Query.set_context(context)
|
|> Query.set_context(context)
|
||||||
|> Query.for_read(strategy.sign_in_action_name, params)
|
|> Query.for_read(strategy.sign_in_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
@ -101,13 +101,13 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
"""
|
"""
|
||||||
@spec sign_in_with_token(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
@spec sign_in_with_token(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
||||||
def sign_in_with_token(strategy, params, options) when is_struct(strategy, Password) do
|
def sign_in_with_token(strategy, params, options) when is_struct(strategy, Password) do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
||||||
|> Query.for_read(strategy.sign_in_with_token_action_name, params)
|
|> Query.for_read(strategy.sign_in_with_token_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
@ -147,7 +147,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
@spec register(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
@spec register(Password.t(), map, keyword) :: {:ok, Resource.record()} | {:error, any}
|
||||||
def register(strategy, params, options)
|
def register(strategy, params, options)
|
||||||
when is_struct(strategy, Password) and strategy.registration_enabled? == true do
|
when is_struct(strategy, Password) and strategy.registration_enabled? == true do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Changeset.new()
|
|> Changeset.new()
|
||||||
|
@ -157,7 +157,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Changeset.for_create(strategy.register_action_name, params)
|
|> Changeset.for_create(strategy.register_action_name, params)
|
||||||
|> api.create(options)
|
|> domain.create(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
def register(strategy, _params, _options) when is_struct(strategy, Password) do
|
def register(strategy, _params, _options) when is_struct(strategy, Password) do
|
||||||
|
@ -182,7 +182,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
params,
|
params,
|
||||||
options
|
options
|
||||||
) do
|
) do
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|
@ -192,7 +192,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Query.for_read(resettable.request_password_reset_action_name, params)
|
|> Query.for_read(resettable.request_password_reset_action_name, params)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -216,7 +216,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
with {:ok, token} <- Map.fetch(params, "reset_token"),
|
with {:ok, token} <- Map.fetch(params, "reset_token"),
|
||||||
{:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, strategy.resource),
|
{:ok, %{"sub" => subject}, resource} <- Jwt.verify(token, strategy.resource),
|
||||||
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource, options) do
|
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource, options) do
|
||||||
api = Info.authentication_api!(resource)
|
domain = Info.domain!(resource)
|
||||||
|
|
||||||
user
|
user
|
||||||
|> Changeset.new()
|
|> Changeset.new()
|
||||||
|
@ -226,7 +226,7 @@ defmodule AshAuthentication.Strategy.Password.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Changeset.for_update(resettable.password_reset_action_name, params)
|
|> Changeset.for_update(resettable.password_reset_action_name, params)
|
||||||
|> api.update(options)
|
|> domain.update(options)
|
||||||
else
|
else
|
||||||
{:error, %Changeset{} = changeset} -> {:error, changeset}
|
{:error, %Changeset{} = changeset} -> {:error, changeset}
|
||||||
_ -> {:error, Errors.InvalidToken.exception(type: :reset)}
|
_ -> {:error, Errors.InvalidToken.exception(type: :reset)}
|
||||||
|
|
|
@ -28,7 +28,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
alias Ash.{Changeset, Error.Changes.InvalidArgument, Error.Framework.AssumptionFailed}
|
|
||||||
|
alias Ash.{
|
||||||
|
Changeset,
|
||||||
|
Error.Changes.InvalidArgument,
|
||||||
|
Error.Framework.AssumptionFailed,
|
||||||
|
Resource.Validation
|
||||||
|
}
|
||||||
|
|
||||||
alias AshAuthentication.Info
|
alias AshAuthentication.Info
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -36,8 +43,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidation do
|
||||||
equivalent values - if confirmation is required.
|
equivalent values - if confirmation is required.
|
||||||
"""
|
"""
|
||||||
@impl true
|
@impl true
|
||||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, String.t() | Exception.t()}
|
@spec validate(Changeset.t(), keyword, Validation.Context.t()) ::
|
||||||
def validate(changeset, options) do
|
:ok | {:error, String.t() | Exception.t()}
|
||||||
|
def validate(changeset, options, _context) do
|
||||||
case Info.find_strategy(changeset, options) do
|
case Info.find_strategy(changeset, options) do
|
||||||
{:ok, %{confirmation_required?: true} = strategy} ->
|
{:ok, %{confirmation_required?: true} = strategy} ->
|
||||||
validate_password_confirmation(changeset, strategy)
|
validate_password_confirmation(changeset, strategy)
|
||||||
|
|
|
@ -43,14 +43,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidation do
|
||||||
|
|
||||||
"""
|
"""
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
alias Ash.Changeset
|
alias Ash.{Changeset, Resource.Validation}
|
||||||
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
|
alias AshAuthentication.{Errors.AuthenticationFailed, Info}
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()}
|
@spec validate(Changeset.t(), keyword, Validation.Context.t()) :: :ok | {:error, Exception.t()}
|
||||||
def validate(changeset, options) do
|
def validate(changeset, options, _context) do
|
||||||
{:ok, strategy} = get_strategy(changeset, options)
|
{:ok, strategy} = get_strategy(changeset, options)
|
||||||
|
|
||||||
with {:ok, password_arg} <- get_password_arg(changeset, options, strategy),
|
with {:ok, password_arg} <- get_password_arg(changeset, options, strategy),
|
||||||
|
|
|
@ -18,7 +18,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _opts, _context) do
|
def prepare(query, _opts, _context) do
|
||||||
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
strategy = Info.strategy_for_action!(query.resource, query.action.name)
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ defmodule AshAuthentication.Strategy.Password.RequestPasswordResetPreparation do
|
||||||
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
select_for_senders = Info.authentication_select_for_senders!(query.resource)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(^ref(identity_field) == ^identity)
|
||||||
|> Query.before_action(fn query ->
|
|> Query.before_action(fn query ->
|
||||||
Ash.Query.ensure_selected(query, select_for_senders)
|
Ash.Query.ensure_selected(query, select_for_senders)
|
||||||
end)
|
end)
|
||||||
|
|
|
@ -4,13 +4,13 @@ defmodule AshAuthentication.Strategy.Password.ResetTokenValidation do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
use Ash.Resource.Validation
|
use Ash.Resource.Validation
|
||||||
alias Ash.{Changeset, Error.Changes.InvalidArgument}
|
alias Ash.{Changeset, Error.Changes.InvalidArgument, Resource.Validation}
|
||||||
alias AshAuthentication.{Info, Jwt}
|
alias AshAuthentication.{Info, Jwt}
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec validate(Changeset.t(), keyword) :: :ok | {:error, Exception.t()}
|
@spec validate(Changeset.t(), keyword, Validation.Context.t()) :: :ok | {:error, Exception.t()}
|
||||||
def validate(changeset, _) do
|
def validate(changeset, _, _) do
|
||||||
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
with {:ok, strategy} <- Info.strategy_for_action(changeset.resource, changeset.action.name),
|
||||||
token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token),
|
token when is_binary(token) <- Changeset.get_argument(changeset, :reset_token),
|
||||||
{:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource),
|
{:ok, %{"act" => token_action}, _} <- Jwt.verify(token, changeset.resource),
|
||||||
|
|
|
@ -19,14 +19,14 @@ defmodule AshAuthentication.Strategy.Password.SignInPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, options, context) do
|
def prepare(query, options, context) do
|
||||||
{:ok, strategy} = Info.find_strategy(query, context, options)
|
{:ok, strategy} = Info.find_strategy(query, context, options)
|
||||||
identity_field = strategy.identity_field
|
identity_field = strategy.identity_field
|
||||||
identity = Query.get_argument(query, identity_field)
|
identity = Query.get_argument(query, identity_field)
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Query.filter(ref(^identity_field) == ^identity)
|
|> Query.filter(^ref(identity_field) == ^identity)
|
||||||
|> check_sign_in_token_configuration(strategy)
|
|> check_sign_in_token_configuration(strategy)
|
||||||
|> Query.before_action(fn query ->
|
|> Query.before_action(fn query ->
|
||||||
Ash.Query.ensure_selected(query, [strategy.hashed_password_field])
|
Ash.Query.ensure_selected(query, [strategy.hashed_password_field])
|
||||||
|
|
|
@ -12,7 +12,7 @@ defmodule AshAuthentication.Strategy.Password.SignInWithTokenPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, options, context) do
|
def prepare(query, options, context) do
|
||||||
{:ok, strategy} = Info.find_strategy(query, context, options)
|
{:ok, strategy} = Info.find_strategy(query, context, options)
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,8 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
||||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, identity_field),
|
{:ok, attribute} <- find_attribute(dsl_state, identity_field),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
|
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
|
||||||
|
:ok <- validate_attribute_option(attribute, resource, :public?, [true]) do
|
||||||
validate_attribute_unique_constraint(dsl_state, [identity_field], resource)
|
validate_attribute_unique_constraint(dsl_state, [identity_field], resource)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -104,8 +105,9 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
||||||
defp validate_hashed_password_field(hashed_password_field, dsl_state) do
|
defp validate_hashed_password_field(hashed_password_field, dsl_state) do
|
||||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, hashed_password_field),
|
{:ok, attribute} <- find_attribute(dsl_state, hashed_password_field),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
||||||
validate_attribute_option(attribute, resource, :sensitive?, [true])
|
:ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]) do
|
||||||
|
validate_attribute_option(attribute, resource, :public?, [false])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -498,7 +500,8 @@ defmodule AshAuthentication.Strategy.Password.Transformer do
|
||||||
arguments: arguments,
|
arguments: arguments,
|
||||||
changes: changes,
|
changes: changes,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
accept: []
|
accept: [],
|
||||||
|
require_atomic?: false
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -120,7 +120,7 @@ defprotocol AshAuthentication.Strategy do
|
||||||
|
|
||||||
See `actions/1` for a list of actions provided by the strategy.
|
See `actions/1` for a list of actions provided by the strategy.
|
||||||
|
|
||||||
Any options passed to the action will be passed to the underlying `Ash.Api` function.
|
Any options passed to the action will be passed to the underlying `Ash.Domain` function.
|
||||||
"""
|
"""
|
||||||
@spec action(t, action, params :: map, options :: keyword) ::
|
@spec action(t, action, params :: map, options :: keyword) ::
|
||||||
:ok | {:ok, Resource.record()} | {:error, any}
|
:ok | {:ok, Resource.record()} | {:error, any}
|
||||||
|
|
|
@ -41,7 +41,7 @@ defmodule AshAuthentication.Supervisor do
|
||||||
raise """
|
raise """
|
||||||
No otp_app provided to AshAuthentication.Supervisor.
|
No otp_app provided to AshAuthentication.Supervisor.
|
||||||
|
|
||||||
In order to find your Ash APIs and resources you need to provide the
|
In order to find your Ash domains and resources you need to provide the
|
||||||
name of your OTP application when starting AshAuthentication.Supervisor:
|
name of your OTP application when starting AshAuthentication.Supervisor:
|
||||||
|
|
||||||
Suggestion, try adding `{AshAuthentication.Supervisor, otp_app: :my_app}`
|
Suggestion, try adding `{AshAuthentication.Supervisor, otp_app: :my_app}`
|
||||||
|
|
|
@ -5,12 +5,13 @@ defmodule AshAuthentication.TokenResource do
|
||||||
%Spark.Dsl.Section{
|
%Spark.Dsl.Section{
|
||||||
name: :token,
|
name: :token,
|
||||||
describe: "Configuration options for this token resource",
|
describe: "Configuration options for this token resource",
|
||||||
modules: [:api],
|
modules: [:domain],
|
||||||
schema: [
|
schema: [
|
||||||
api: [
|
domain: [
|
||||||
type: {:behaviour, Ash.Api},
|
type: {:behaviour, Ash.Domain},
|
||||||
|
required: false,
|
||||||
doc: """
|
doc: """
|
||||||
The Ash API to use to access this resource.
|
The Ash domain to use to access this resource.
|
||||||
"""
|
"""
|
||||||
],
|
],
|
||||||
expunge_expired_action_name: [
|
expunge_expired_action_name: [
|
||||||
|
@ -117,11 +118,8 @@ defmodule AshAuthentication.TokenResource do
|
||||||
defmodule MyApp.Accounts.Token do
|
defmodule MyApp.Accounts.Token do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.TokenResource]
|
extensions: [AshAuthentication.TokenResource],
|
||||||
|
domain: MyApp.Accounts
|
||||||
token do
|
|
||||||
api MyApp.Accounts
|
|
||||||
end
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "tokens"
|
table "tokens"
|
||||||
|
|
|
@ -12,7 +12,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec read_expired(Resource.t(), keyword) :: {:ok, [Resource.record()]} | {:error, any}
|
@spec read_expired(Resource.t(), keyword) :: {:ok, [Resource.record()]} | {:error, any}
|
||||||
def read_expired(resource, opts \\ []) do
|
def read_expired(resource, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do
|
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|
@ -22,7 +22,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Query.for_read(read_expired_action_name, opts)
|
|> Query.for_read(read_expired_action_name, opts)
|
||||||
|> api.read()
|
|> domain.read()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec token_revoked?(Resource.t(), String.t(), keyword) :: boolean
|
@spec token_revoked?(Resource.t(), String.t(), keyword) :: boolean
|
||||||
def token_revoked?(resource, token, opts \\ []) do
|
def token_revoked?(resource, token, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
|
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|
@ -80,7 +80,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Query.for_read(is_revoked_action_name, %{"token" => token}, opts)
|
|> Query.for_read(is_revoked_action_name, %{"token" => token}, opts)
|
||||||
|> api.read()
|
|> domain.read()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, []} -> false
|
{:ok, []} -> false
|
||||||
{:ok, _} -> true
|
{:ok, _} -> true
|
||||||
|
@ -98,7 +98,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec jti_revoked?(Resource.t(), String.t(), keyword) :: boolean
|
@spec jti_revoked?(Resource.t(), String.t(), keyword) :: boolean
|
||||||
def jti_revoked?(resource, jti, opts \\ []) do
|
def jti_revoked?(resource, jti, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
|
{:ok, is_revoked_action_name} <- Info.token_revocation_is_revoked_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|
@ -108,7 +108,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|> Query.for_read(is_revoked_action_name, %{"jti" => jti}, opts)
|
|> Query.for_read(is_revoked_action_name, %{"jti" => jti}, opts)
|
||||||
|> api.read()
|
|> domain.read()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, []} -> false
|
{:ok, []} -> false
|
||||||
{:ok, _} -> true
|
{:ok, _} -> true
|
||||||
|
@ -130,7 +130,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec revoke(Resource.t(), String.t(), keyword) :: :ok | {:error, any}
|
@spec revoke(Resource.t(), String.t(), keyword) :: :ok | {:error, any}
|
||||||
def revoke(resource, token, opts \\ []) do
|
def revoke(resource, token, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, revoke_token_action_name} <-
|
{:ok, revoke_token_action_name} <-
|
||||||
Info.token_revocation_revoke_token_action_name(resource) do
|
Info.token_revocation_revoke_token_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|
@ -145,7 +145,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
%{"token" => token},
|
%{"token" => token},
|
||||||
Keyword.merge(opts, upsert?: true)
|
Keyword.merge(opts, upsert?: true)
|
||||||
)
|
)
|
||||||
|> api.create()
|
|> domain.create()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -161,7 +161,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec store_token(Resource.t(), map, keyword) :: :ok | {:error, any}
|
@spec store_token(Resource.t(), map, keyword) :: :ok | {:error, any}
|
||||||
def store_token(resource, params, opts \\ []) do
|
def store_token(resource, params, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, store_token_action_name} <- Info.token_store_token_action_name(resource) do
|
{:ok, store_token_action_name} <- Info.token_store_token_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|> Changeset.new()
|
|> Changeset.new()
|
||||||
|
@ -175,7 +175,7 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
params,
|
params,
|
||||||
Keyword.merge(opts, upsert?: true)
|
Keyword.merge(opts, upsert?: true)
|
||||||
)
|
)
|
||||||
|> api.create()
|
|> domain.create()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -189,45 +189,29 @@ defmodule AshAuthentication.TokenResource.Actions do
|
||||||
@spec get_token(Resource.t(), map, keyword) :: {:ok, [Resource.record()]} | {:error, any}
|
@spec get_token(Resource.t(), map, keyword) :: {:ok, [Resource.record()]} | {:error, any}
|
||||||
def get_token(resource, params, opts \\ []) do
|
def get_token(resource, params, opts \\ []) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, get_token_action_name} <- Info.token_get_token_action_name(resource) do
|
{:ok, get_token_action_name} <- Info.token_get_token_action_name(resource) do
|
||||||
resource
|
resource
|
||||||
|> Query.new()
|
|> Query.new()
|
||||||
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
||||||
|> Query.for_read(get_token_action_name, params, opts)
|
|> Query.for_read(get_token_action_name, params, opts)
|
||||||
|> api.read()
|
|> domain.read()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp expunge_inside_transaction(resource, expunge_expired_action_name, opts) do
|
defp expunge_inside_transaction(resource, expunge_expired_action_name, opts) do
|
||||||
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
with :ok <- assert_resource_has_extension(resource, TokenResource),
|
||||||
{:ok, api} <- Info.token_api(resource),
|
{:ok, domain} <- Info.token_domain(resource),
|
||||||
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource),
|
{:ok, read_expired_action_name} <- Info.token_read_expired_action_name(resource) do
|
||||||
query <-
|
resource
|
||||||
resource |> Query.new() |> Query.set_context(%{private: %{ash_authentication?: true}}),
|
|> Query.new()
|
||||||
query <- Query.for_read(query, read_expired_action_name, opts),
|
|> Query.set_context(%{private: %{ash_authentication?: true}})
|
||||||
{:ok, expired} <- api.read(query) do
|
|> Query.for_read(read_expired_action_name, opts)
|
||||||
Enum.reduce_while(expired, {:ok, []}, fn record, {:ok, notifications} ->
|
|> domain.bulk_destroy(expunge_expired_action_name, %{}, opts)
|
||||||
record
|
|> case do
|
||||||
|> Changeset.new()
|
%{status: :success, notifications: notifications} -> {:ok, notifications}
|
||||||
|> Changeset.set_context(%{
|
%{errors: errors} -> {:error, Ash.Error.to_class(errors)}
|
||||||
private: %{
|
end
|
||||||
ash_authentication?: true
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|> Changeset.for_destroy(expunge_expired_action_name, opts)
|
|
||||||
|> api.destroy(return_notifications?: true)
|
|
||||||
|> case do
|
|
||||||
:ok ->
|
|
||||||
{:cont, {:ok, notifications}}
|
|
||||||
|
|
||||||
{:ok, more_notifications} ->
|
|
||||||
{:cont, {:ok, Enum.concat(notifications, more_notifications)}}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
{:halt, {:error, reason}}
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,10 +8,10 @@ defmodule AshAuthentication.TokenResource.Expunger do
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.Accounts.Token do
|
defmodule MyApp.Accounts.Token do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
extensions: [AshAuthentication.TokenResource]
|
extensions: [AshAuthentication.TokenResource],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
token do
|
token do
|
||||||
api MyApp.Accounts
|
|
||||||
expunge_interval 12
|
expunge_interval 12
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetConfirmationChangesPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _, _) do
|
def prepare(query, _, _) do
|
||||||
jti = Query.get_argument(query, :jti)
|
jti = Query.get_argument(query, :jti)
|
||||||
strategy = query.context.strategy
|
strategy = query.context.strategy
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.GetTokenPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _, _) do
|
def prepare(query, _, _) do
|
||||||
jti = get_jti(query)
|
jti = get_jti(query)
|
||||||
purpose = Query.get_argument(query, :purpose)
|
purpose = Query.get_argument(query, :purpose)
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule AshAuthentication.TokenResource.IsRevokedPreparation do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
@impl true
|
@impl true
|
||||||
@spec prepare(Query.t(), keyword, Preparation.context()) :: Query.t()
|
@spec prepare(Query.t(), keyword, Preparation.Context.t()) :: Query.t()
|
||||||
def prepare(query, _opts, _context) do
|
def prepare(query, _opts, _context) do
|
||||||
case get_jti(query) do
|
case get_jti(query) do
|
||||||
{:ok, jti} ->
|
{:ok, jti} ->
|
||||||
|
|
|
@ -34,13 +34,14 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
@spec transform(map) ::
|
@spec transform(map) ::
|
||||||
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
||||||
def transform(dsl_state) do
|
def transform(dsl_state) do
|
||||||
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :token),
|
with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :token),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :jti, :string,
|
maybe_build_attribute(dsl_state, :jti, :string,
|
||||||
primary_key?: true,
|
primary_key?: true,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
sensitive?: true,
|
sensitive?: true,
|
||||||
writable?: true
|
writable?: true,
|
||||||
|
public?: true
|
||||||
),
|
),
|
||||||
:ok <- validate_jti_field(dsl_state),
|
:ok <- validate_jti_field(dsl_state),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
|
@ -55,24 +56,26 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :purpose, :string,
|
maybe_build_attribute(dsl_state, :purpose, :string,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
writable?: true
|
writable?: true,
|
||||||
|
public?: true
|
||||||
),
|
),
|
||||||
:ok <- validate_purpose_field(dsl_state),
|
:ok <- validate_purpose_field(dsl_state),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :extra_data, :map,
|
maybe_build_attribute(dsl_state, :extra_data, :map,
|
||||||
allow_nil?: true,
|
allow_nil?: true,
|
||||||
writable?: true
|
writable?: true,
|
||||||
|
public?: true
|
||||||
),
|
),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec,
|
maybe_build_attribute(dsl_state, :created_at, :utc_datetime_usec,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
private?: true,
|
public?: false,
|
||||||
default: &DateTime.utc_now/0
|
default: &DateTime.utc_now/0
|
||||||
),
|
),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
|
maybe_build_attribute(dsl_state, :updated_at, :utc_datetime_usec,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
private?: true,
|
public?: false,
|
||||||
default: &DateTime.utc_now/0,
|
default: &DateTime.utc_now/0,
|
||||||
update_default: &DateTime.utc_now/0
|
update_default: &DateTime.utc_now/0
|
||||||
),
|
),
|
||||||
|
@ -311,11 +314,16 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_read_expired_action(_dsl_state, action_name) do
|
defp build_read_expired_action(_dsl_state, action_name) do
|
||||||
import Ash.Filter.TemplateHelpers
|
import Ash.Expr
|
||||||
|
|
||||||
|
filter =
|
||||||
|
Transformer.build_entity!(Resource.Dsl, [:actions, :read], :filter,
|
||||||
|
filter: expr(expires_at < now())
|
||||||
|
)
|
||||||
|
|
||||||
Transformer.build_entity(Resource.Dsl, [:actions], :read,
|
Transformer.build_entity(Resource.Dsl, [:actions], :read,
|
||||||
name: action_name,
|
name: action_name,
|
||||||
filter: expr(expires_at < now())
|
filters: [filter]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -421,7 +429,7 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
:ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]),
|
:ok <- validate_attribute_option(attribute, resource, :sensitive?, [true]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
|
:ok <- validate_attribute_option(attribute, resource, :primary_key?, [true]) do
|
||||||
validate_attribute_option(attribute, resource, :private?, [false])
|
validate_attribute_option(attribute, resource, :public?, [true])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -439,8 +447,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, :purpose),
|
{:ok, attribute} <- find_attribute(dsl_state, :purpose),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
|
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]) do
|
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
|
||||||
validate_attribute_option(attribute, resource, :writable?, [true])
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
|
||||||
|
validate_attribute_option(attribute, resource, :public?, [true])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -448,8 +457,9 @@ defmodule AshAuthentication.TokenResource.Transformer do
|
||||||
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
with {:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, :extra_data),
|
{:ok, attribute} <- find_attribute(dsl_state, :extra_data),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
|
:ok <- validate_attribute_option(attribute, resource, :type, [Type.Map, :map]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]) do
|
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [true]),
|
||||||
validate_attribute_option(attribute, resource, :writable?, [true])
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
|
||||||
|
validate_attribute_option(attribute, resource, :public?, [true])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -28,28 +28,28 @@ defmodule AshAuthentication.TokenResource.Verifier do
|
||||||
@spec transform(map) ::
|
@spec transform(map) ::
|
||||||
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
||||||
def transform(dsl_state) do
|
def transform(dsl_state) do
|
||||||
validate_api_presence(dsl_state)
|
validate_domain_presence(dsl_state)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_api_presence(dsl_state) do
|
defp validate_domain_presence(dsl_state) do
|
||||||
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:token], :api),
|
with domain when not is_nil(domain) <- Transformer.get_option(dsl_state, [:token], :domain),
|
||||||
:ok <- assert_is_module(api),
|
:ok <- assert_is_module(domain),
|
||||||
true <- function_exported?(api, :spark_is, 0),
|
true <- function_exported?(domain, :spark_is, 0),
|
||||||
Ash.Api <- api.spark_is() do
|
Ash.Domain <- domain.spark_is() do
|
||||||
{:ok, api}
|
{:ok, domain}
|
||||||
else
|
else
|
||||||
nil ->
|
nil ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
path: [:token, :api],
|
path: [:token, :domain],
|
||||||
message: "An API module must be present"
|
message: "A domain module must be present"
|
||||||
)}
|
)}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
path: [:token, :api],
|
path: [:token, :domain],
|
||||||
message: "Module is not an Ash.Api."
|
message: "Module is not an `Ash.Domain`."
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -30,7 +30,7 @@ defmodule AshAuthentication.Transformer do
|
||||||
@spec transform(map) ::
|
@spec transform(map) ::
|
||||||
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
||||||
def transform(dsl_state) do
|
def transform(dsl_state) do
|
||||||
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :authentication),
|
with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :authentication),
|
||||||
:ok <- validate_at_least_one_strategy(dsl_state),
|
:ok <- validate_at_least_one_strategy(dsl_state),
|
||||||
:ok <- validate_unique_strategy_names(dsl_state),
|
:ok <- validate_unique_strategy_names(dsl_state),
|
||||||
:ok <- validate_unique_add_on_names(dsl_state),
|
:ok <- validate_unique_add_on_names(dsl_state),
|
||||||
|
|
|
@ -3,11 +3,12 @@ defmodule AshAuthentication.UserIdentity do
|
||||||
%Spark.Dsl.Section{
|
%Spark.Dsl.Section{
|
||||||
name: :user_identity,
|
name: :user_identity,
|
||||||
describe: "Configure identity options for this resource",
|
describe: "Configure identity options for this resource",
|
||||||
modules: [:api, :user_resource],
|
modules: [:domain, :user_resource],
|
||||||
schema: [
|
schema: [
|
||||||
api: [
|
domain: [
|
||||||
type: {:behaviour, Ash.Api},
|
type: {:behaviour, Ash.Domain},
|
||||||
doc: "The Ash API to use to access this resource."
|
doc: "The Ash domain to use to access this resource.",
|
||||||
|
required: false
|
||||||
],
|
],
|
||||||
user_resource: [
|
user_resource: [
|
||||||
type: {:behaviour, Ash.Resource},
|
type: {:behaviour, Ash.Resource},
|
||||||
|
@ -95,10 +96,10 @@ defmodule AshAuthentication.UserIdentity do
|
||||||
defmodule MyApp.Accounts.UserIdentity do
|
defmodule MyApp.Accounts.UserIdentity do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.UserIdentity]
|
extensions: [AshAuthentication.UserIdentity],
|
||||||
|
domain: MyApp.Accounts
|
||||||
|
|
||||||
user_identity do
|
user_identity do
|
||||||
api MyApp.Accounts
|
|
||||||
user_resource MyApp.Accounts.User
|
user_resource MyApp.Accounts.User
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
|
||||||
Code interface for provider identity actions.
|
Code interface for provider identity actions.
|
||||||
|
|
||||||
Allows you to interact with UserIdentity resources without having to mess
|
Allows you to interact with UserIdentity resources without having to mess
|
||||||
around with changesets, apis, etc. These functions are delegated to from
|
around with changesets, domains, etc. These functions are delegated to from
|
||||||
within `AshAuthentication.UserIdentity`.
|
within `AshAuthentication.UserIdentity`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
|
||||||
"""
|
"""
|
||||||
@spec upsert(Resource.t(), map) :: {:ok, Resource.record()} | {:error, term}
|
@spec upsert(Resource.t(), map) :: {:ok, Resource.record()} | {:error, term}
|
||||||
def upsert(resource, attributes) do
|
def upsert(resource, attributes) do
|
||||||
with {:ok, api} <- UserIdentity.Info.user_identity_api(resource),
|
with {:ok, domain} <- UserIdentity.Info.user_identity_domain(resource),
|
||||||
{:ok, upsert_action_name} <-
|
{:ok, upsert_action_name} <-
|
||||||
UserIdentity.Info.user_identity_upsert_action_name(resource),
|
UserIdentity.Info.user_identity_upsert_action_name(resource),
|
||||||
action when is_map(action) <- Resource.Info.action(resource, upsert_action_name) do
|
action when is_map(action) <- Resource.Info.action(resource, upsert_action_name) do
|
||||||
|
@ -30,7 +30,7 @@ defmodule AshAuthentication.UserIdentity.Actions do
|
||||||
upsert?: true,
|
upsert?: true,
|
||||||
upsert_identity: action.upsert_identity
|
upsert_identity: action.upsert_identity
|
||||||
)
|
)
|
||||||
|> api.create()
|
|> domain.create()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -34,7 +34,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
@spec transform(map) ::
|
@spec transform(map) ::
|
||||||
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
||||||
def transform(dsl_state) do
|
def transform(dsl_state) do
|
||||||
with {:ok, dsl_state} <- maybe_set_api(dsl_state, :user_identity),
|
with {:ok, dsl_state} <- maybe_set_domain(dsl_state, :user_identity),
|
||||||
{:ok, resource} <- persisted_option(dsl_state, :module),
|
{:ok, resource} <- persisted_option(dsl_state, :module),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, :id, Type.UUID,
|
maybe_build_attribute(dsl_state, :id, Type.UUID,
|
||||||
|
@ -58,7 +58,8 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
maybe_build_attribute(dsl_state, strategy, Type.String,
|
maybe_build_attribute(dsl_state, strategy, Type.String,
|
||||||
allow_nil?: false,
|
allow_nil?: false,
|
||||||
writable?: true
|
writable?: true,
|
||||||
|
public?: true
|
||||||
),
|
),
|
||||||
:ok <- validate_strategy_field(dsl_state, strategy),
|
:ok <- validate_strategy_field(dsl_state, strategy),
|
||||||
{:ok, dsl_state} <-
|
{:ok, dsl_state} <-
|
||||||
|
@ -134,7 +135,8 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
{:ok, attribute} <- find_attribute(dsl_state, field_name),
|
{:ok, attribute} <- find_attribute(dsl_state, field_name),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
|
:ok <- validate_attribute_option(attribute, resource, :type, [Type.String, :string]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
|
:ok <- validate_attribute_option(attribute, resource, :allow_nil?, [false]),
|
||||||
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]) do
|
:ok <- validate_attribute_option(attribute, resource, :writable?, [true]),
|
||||||
|
:ok <- validate_attribute_option(attribute, resource, :public?, [true]) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
@ -183,7 +185,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
|
|
||||||
defp build_user_relationship(dsl_state, name, destination) do
|
defp build_user_relationship(dsl_state, name, destination) do
|
||||||
with {:ok, id_attr} <- find_pk(destination),
|
with {:ok, id_attr} <- find_pk(destination),
|
||||||
{:ok, api} <- AshAuthentication.Info.authentication_api(destination),
|
{:ok, domain} <- AshAuthentication.Info.domain(destination),
|
||||||
{:ok, user_id} <-
|
{:ok, user_id} <-
|
||||||
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state) do
|
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state) do
|
||||||
Transformer.build_entity(Resource.Dsl, [:relationships], :belongs_to,
|
Transformer.build_entity(Resource.Dsl, [:relationships], :belongs_to,
|
||||||
|
@ -193,7 +195,7 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
destination_attribute: id_attr.name,
|
destination_attribute: id_attr.name,
|
||||||
attribute_type: id_attr.type,
|
attribute_type: id_attr.type,
|
||||||
source_attribute: user_id,
|
source_attribute: user_id,
|
||||||
api: api,
|
domain: domain,
|
||||||
attribute_writable?: true,
|
attribute_writable?: true,
|
||||||
writable?: true
|
writable?: true
|
||||||
)
|
)
|
||||||
|
@ -202,14 +204,14 @@ defmodule AshAuthentication.UserIdentity.Transformer do
|
||||||
|
|
||||||
defp validate_user_relationship(dsl_state, name, destination) do
|
defp validate_user_relationship(dsl_state, name, destination) do
|
||||||
with {:ok, id_attr} <- find_pk(destination),
|
with {:ok, id_attr} <- find_pk(destination),
|
||||||
{:ok, api} <- AshAuthentication.Info.authentication_api(destination),
|
{:ok, domain} <- AshAuthentication.Info.domain(destination),
|
||||||
{:ok, relationship} <- find_relationship(dsl_state, name),
|
{:ok, relationship} <- find_relationship(dsl_state, name),
|
||||||
{:ok, user_id} <-
|
{:ok, user_id} <-
|
||||||
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
|
UserIdentity.Info.user_identity_user_id_attribute_name(dsl_state),
|
||||||
:ok <- validate_field_in_values(relationship, :destination, [destination]),
|
:ok <- validate_field_in_values(relationship, :destination, [destination]),
|
||||||
:ok <- validate_field_in_values(relationship, :destination_attribute, [id_attr.name]),
|
:ok <- validate_field_in_values(relationship, :destination_attribute, [id_attr.name]),
|
||||||
:ok <- validate_field_in_values(relationship, :source_attribute, [user_id]),
|
:ok <- validate_field_in_values(relationship, :source_attribute, [user_id]),
|
||||||
:ok <- validate_field_in_values(relationship, :api, [api]) do
|
:ok <- validate_field_in_values(relationship, :domain, [domain]) do
|
||||||
validate_field_in_values(relationship, :attribute_type, [id_attr.type])
|
validate_field_in_values(relationship, :attribute_type, [id_attr.type])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,14 +27,14 @@ defmodule AshAuthentication.UserIdentity.Verifier do
|
||||||
@spec transform(map) ::
|
@spec transform(map) ::
|
||||||
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
:ok | {:ok, map} | {:error, term} | {:warn, map, String.t() | [String.t()]} | :halt
|
||||||
def transform(dsl_state) do
|
def transform(dsl_state) do
|
||||||
with :ok <- validate_api_presence(dsl_state) do
|
with :ok <- validate_domain_presence(dsl_state) do
|
||||||
validate_user_resource(dsl_state)
|
validate_user_resource(dsl_state)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_api_presence(dsl_state) do
|
defp validate_domain_presence(dsl_state) do
|
||||||
with {:ok, api} <- Info.user_identity_api(dsl_state) do
|
with {:ok, domain} <- Info.user_identity_domain(dsl_state) do
|
||||||
assert_is_api(api)
|
assert_is_domain(domain)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
defmodule AshAuthentication.Utils do
|
defmodule AshAuthentication.Utils do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
alias Ash.{Api, Resource}
|
alias Ash.{Domain, Resource}
|
||||||
alias Spark.{Dsl, Dsl.Transformer}
|
alias Spark.{Dsl, Dsl.Transformer}
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -80,13 +80,13 @@ defmodule AshAuthentication.Utils do
|
||||||
def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements)
|
def maybe_concat(collection, _test, new_elements), do: Enum.concat(collection, new_elements)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Used within transformers to infer `api` from a resource if the option is not set.
|
Used within transformers to infer `domain` from a resource if the option is not set.
|
||||||
"""
|
"""
|
||||||
def maybe_set_api(dsl_state, section) do
|
def maybe_set_domain(dsl_state, section) do
|
||||||
api = Transformer.get_persisted(dsl_state, :api)
|
domain = Transformer.get_persisted(dsl_state, :domain)
|
||||||
|
|
||||||
if api && !Transformer.get_option(dsl_state, [section], :api) do
|
if domain && !Transformer.get_option(dsl_state, [section], :domain) do
|
||||||
{:ok, Transformer.set_option(dsl_state, [section], :api, api)}
|
{:ok, Transformer.set_option(dsl_state, [section], :domain, domain)}
|
||||||
else
|
else
|
||||||
{:ok, dsl_state}
|
{:ok, dsl_state}
|
||||||
end
|
end
|
||||||
|
@ -209,16 +209,16 @@ defmodule AshAuthentication.Utils do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Asserts that `module` is actually an Ash API.
|
Asserts that `module` is actually an Ash domain.
|
||||||
"""
|
"""
|
||||||
@spec assert_is_api(Api.t()) :: :ok | {:error, term}
|
@spec assert_is_domain(Domain.t()) :: :ok | {:error, term}
|
||||||
def assert_is_api(module) do
|
def assert_is_domain(module) do
|
||||||
with :ok <- assert_is_module(module),
|
with :ok <- assert_is_module(module),
|
||||||
true <- function_exported?(module, :spark_is, 0),
|
true <- function_exported?(module, :spark_is, 0),
|
||||||
Api <- module.spark_is() do
|
Domain <- module.spark_is() do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
_ -> {:error, "Module `#{inspect(module)}` is not an Ash API"}
|
_ -> {:error, "Module `#{inspect(module)}` is not an Ash domain"}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -198,4 +198,52 @@ defmodule AshAuthentication.Validations.Action do
|
||||||
"The action `#{inspect(action.name)}` should have the `#{inspect(preparation_module)}` preparation present."
|
"The action `#{inspect(action.name)}` should have the `#{inspect(preparation_module)}` preparation present."
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Validate the action has the provided option.
|
||||||
|
"""
|
||||||
|
@spec validate_action_option(Actions.action(), atom, [any]) :: :ok | {:error, Exception.t()}
|
||||||
|
def validate_action_option(action, field, values) do
|
||||||
|
with {:ok, value} <- Map.fetch(action, field),
|
||||||
|
true <- value in values do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
:error ->
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
path: [:actions, action.name, field],
|
||||||
|
message:
|
||||||
|
"The action `#{inspect(action.name)}` is missing the `#{inspect(field)}` option set"
|
||||||
|
)}
|
||||||
|
|
||||||
|
false ->
|
||||||
|
case values do
|
||||||
|
[] ->
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
path: [:actions, action.name, field],
|
||||||
|
message:
|
||||||
|
"The action `#{inspect(action.name)}` should not have the `#{inspect(field)}` option set"
|
||||||
|
)}
|
||||||
|
|
||||||
|
[expected] ->
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
path: [:actions, action.name, field],
|
||||||
|
message:
|
||||||
|
"The action `#{inspect(action.name)}` should have the `#{inspect(field)}` option set to `#{inspect(expected)}`"
|
||||||
|
)}
|
||||||
|
|
||||||
|
expected ->
|
||||||
|
expected = expected |> Enum.map(&"`#{inspect(&1)}`") |> to_sentence(final: "or")
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
DslError.exception(
|
||||||
|
path: [:actions, action.name, field],
|
||||||
|
message:
|
||||||
|
"The action `#{inspect(action.name)}` should have the `#{inspect(field)}` option set to one of #{expected}"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -19,6 +19,7 @@ defmodule AshAuthentication.Validations.Attribute do
|
||||||
:error ->
|
:error ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
|
module: resource,
|
||||||
path: [:actions, :attribute],
|
path: [:actions, :attribute],
|
||||||
message:
|
message:
|
||||||
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is missing the `#{inspect(field)}` property"
|
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is missing the `#{inspect(field)}` property"
|
||||||
|
@ -29,6 +30,7 @@ defmodule AshAuthentication.Validations.Attribute do
|
||||||
[] ->
|
[] ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
|
module: resource,
|
||||||
path: [:actions, :attribute],
|
path: [:actions, :attribute],
|
||||||
message:
|
message:
|
||||||
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is should not have `#{inspect(field)}` set"
|
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource is should not have `#{inspect(field)}` set"
|
||||||
|
@ -37,6 +39,7 @@ defmodule AshAuthentication.Validations.Attribute do
|
||||||
[expected] ->
|
[expected] ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
|
module: resource,
|
||||||
path: [:actions, :attribute],
|
path: [:actions, :attribute],
|
||||||
message:
|
message:
|
||||||
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to `#{inspect(expected)}`"
|
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to `#{inspect(expected)}`"
|
||||||
|
@ -47,6 +50,7 @@ defmodule AshAuthentication.Validations.Attribute do
|
||||||
|
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
|
module: resource,
|
||||||
path: [:actions, :attribute],
|
path: [:actions, :attribute],
|
||||||
message:
|
message:
|
||||||
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to one of #{expected}"
|
"The attribute `#{inspect(attribute.name)}` on the `#{inspect(resource)}` resource should have `#{inspect(field)}` set to one of #{expected}"
|
||||||
|
|
|
@ -17,30 +17,31 @@ defmodule AshAuthentication.Verifier do
|
||||||
| {:error, term}
|
| {:error, term}
|
||||||
| {:warn, String.t() | list(String.t())}
|
| {:warn, String.t() | list(String.t())}
|
||||||
def verify(dsl_state) do
|
def verify(dsl_state) do
|
||||||
with {:ok, _api} <- validate_api_presence(dsl_state) do
|
with {:ok, _domain} <- validate_domain_presence(dsl_state) do
|
||||||
validate_token_resource(dsl_state)
|
validate_token_resource(dsl_state)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_api_presence(dsl_state) do
|
defp validate_domain_presence(dsl_state) do
|
||||||
with api when not is_nil(api) <- Transformer.get_option(dsl_state, [:authentication], :api),
|
with domain when not is_nil(domain) <-
|
||||||
:ok <- assert_is_module(api),
|
Transformer.get_option(dsl_state, [:authentication], :domain),
|
||||||
true <- function_exported?(api, :spark_is, 0),
|
:ok <- assert_is_module(domain),
|
||||||
Ash.Api <- api.spark_is() do
|
true <- function_exported?(domain, :spark_is, 0),
|
||||||
{:ok, api}
|
Ash.Domain <- domain.spark_is() do
|
||||||
|
{:ok, domain}
|
||||||
else
|
else
|
||||||
nil ->
|
nil ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
path: [:authentication, :api],
|
path: [:authentication, :domain],
|
||||||
message: "An API module must be present"
|
message: "A domain module must be present"
|
||||||
)}
|
)}
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
{:error,
|
{:error,
|
||||||
DslError.exception(
|
DslError.exception(
|
||||||
path: [:authentication, :api],
|
path: [:authentication, :domain],
|
||||||
message: "Module is not an Ash.Api."
|
message: "Module is not an `Ash.Domain`."
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
13
mix.exs
13
mix.exs
|
@ -173,7 +173,7 @@ defmodule AshAuthentication.MixProject do
|
||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
{:ash, ash_version("~> 2.5 and >= 2.5.11")},
|
{:ash, ash_version("== 3.0.0-rc.1")},
|
||||||
{:assent, "~> 0.2 and >= 0.2.8"},
|
{:assent, "~> 0.2 and >= 0.2.8"},
|
||||||
{:bcrypt_elixir, "~> 3.0"},
|
{:bcrypt_elixir, "~> 3.0"},
|
||||||
{:castore, "~> 1.0"},
|
{:castore, "~> 1.0"},
|
||||||
|
@ -181,16 +181,17 @@ defmodule AshAuthentication.MixProject do
|
||||||
{:jason, "~> 1.4"},
|
{:jason, "~> 1.4"},
|
||||||
{:joken, "~> 2.5"},
|
{:joken, "~> 2.5"},
|
||||||
{:plug, "~> 1.13"},
|
{:plug, "~> 1.13"},
|
||||||
{:spark, "~> 1.1 and >= 1.1.39"},
|
{:spark, "~> 2.0"},
|
||||||
|
{:splode, "~> 0.2"},
|
||||||
{:absinthe_plug, "~> 1.5", only: [:dev, :test]},
|
{:absinthe_plug, "~> 1.5", only: [:dev, :test]},
|
||||||
{:ash_graphql, "~> 0.21", only: [:dev, :test]},
|
# {:ash_graphql, "~> 0.21", only: [:dev, :test]},
|
||||||
{:ash_json_api, "~> 0.30", only: [:dev, :test]},
|
# {:ash_json_api, "~> 0.30", only: [:dev, :test]},
|
||||||
{:ash_postgres, "~> 1.5.1", optional: true},
|
{:ash_postgres, "== 2.0.0-rc.1", optional: true},
|
||||||
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.6", only: [:dev, :test], runtime: false},
|
||||||
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
|
{:dialyxir, "~> 1.2", only: [:dev, :test], runtime: false},
|
||||||
{:doctor, "~> 0.18", only: [:dev, :test]},
|
{:doctor, "~> 0.18", only: [:dev, :test]},
|
||||||
{:ex_check, "~> 0.15", only: [:dev, :test]},
|
{:ex_check, "~> 0.15", only: [:dev, :test]},
|
||||||
{:ex_doc, github: "elixir-lang/ex_doc", only: [:dev, :test], runtime: false},
|
{:ex_doc, ">= 0.0.0", only: [:dev, :test]},
|
||||||
{:faker, "~> 0.18.0", only: [:dev, :test]},
|
{:faker, "~> 0.18.0", only: [:dev, :test]},
|
||||||
{:git_ops, "~> 2.4", only: [:dev, :test], runtime: false},
|
{:git_ops, "~> 2.4", only: [:dev, :test], runtime: false},
|
||||||
{:mimic, "~> 1.7", only: [:dev, :test]},
|
{:mimic, "~> 1.7", only: [:dev, :test]},
|
||||||
|
|
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"},
|
"absinthe_plug": {:hex, :absinthe_plug, "1.5.8", "38d230641ba9dca8f72f1fed2dfc8abd53b3907d1996363da32434ab6ee5d6ab", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bbb04176647b735828861e7b2705465e53e2cf54ccf5a73ddd1ebd855f996e5a"},
|
||||||
"ash": {:hex, :ash, "2.21.2", "d62657fc18ee8a519042b03721ab34427ed640d0dbd1ddd79df175dd2876b8f6", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: false]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.6", [hex: :reactor, repo: "hexpm", optional: false]}, {:spark, ">= 1.1.55 and < 2.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b512c51812ef971c1cb2993b55ffc9aa833e251cdbbdbd813657e9533e78c3d9"},
|
"ash": {:hex, :ash, "3.0.0-rc.1", "ab922a15d85f4c2ee2250908239cf8dda3ffdbdcac4664b7814afdd024428046", [:mix], [{:comparable, "~> 1.0", [hex: :comparable, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.8", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.1.7 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 0.6", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf3e579867f3ed15e219789fe47965473b1639aae68c383579fbc4a982e11f85"},
|
||||||
"ash_graphql": {:hex, :ash_graphql, "0.27.1", "514ea4d3f2dafab45d6d4a0c7eb8037719a693a905295575f1faa069b6791b09", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, "~> 2.17", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "81585f7db55730938f263f78d32e35cea81b6d1139b14ce59cdf59e1ab3a688b"},
|
"ash_postgres": {:hex, :ash_postgres, "2.0.0-rc.1", "c6f2284ab5c7271df63cf13af82655c8632d32665809d2120861c38bd9ee32b3", [:mix], [{:ash, "~> 3.0.0-rc.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:simple_sat, "~> 0.1", [hex: :simple_sat, repo: "hexpm", optional: false]}], "hexpm", "840d95c9ac9e363620428568c22a0312aaa181c74f55289a8bc6588801f22e93"},
|
||||||
"ash_json_api": {:hex, :ash_json_api, "0.34.2", "21a1f935d1208d7f419f08cb44ae379ffa9919dc4860e6bbc6e7499762986e7e", [:mix], [{:ash, ">= 2.9.24 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4.0", [hex: :json_xema, repo: "hexpm", optional: false]}, {:open_api_spex, "~> 3.16", [hex: :open_api_spex, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "620658e495ac745807d8eab0e752836f44e1368c98c7beaad5d4c2bd8c286cf4"},
|
|
||||||
"ash_postgres": {:hex, :ash_postgres, "1.5.22", "3cad63ffce8080615240f01caf389876dbbadda61a298585d6dc50fa4d48680f", [:mix], [{:ash, ">= 2.20.3 and < 3.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "1056587a9e6aab2b0ed282a9ab70a7e5f3a059eed7c7546e0265225ad62f41fc"},
|
|
||||||
"assent": {:hex, :assent, "0.2.9", "e3cdbc8f2e4f8d02c4c490ef8c2148bb1bc0d81aa0648f09addc5918d9a1cd5a", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "5f9562bda90bef7bd3f1b9a348520a5631b86c85145346bb7edb8a7ebbad8e86"},
|
"assent": {:hex, :assent, "0.2.9", "e3cdbc8f2e4f8d02c4c490ef8c2148bb1bc0d81aa0648f09addc5918d9a1cd5a", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "5f9562bda90bef7bd3f1b9a348520a5631b86c85145346bb7edb8a7ebbad8e86"},
|
||||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
|
||||||
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
|
"castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"},
|
||||||
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
"comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
|
||||||
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
"comparable": {:hex, :comparable, "1.0.0", "bb669e91cedd14ae9937053e5bcbc3c52bb2f22422611f43b6e38367d94a495f", [:mix], [{:typable, "~> 0.1", [hex: :typable, repo: "hexpm", optional: false]}], "hexpm", "277c11eeb1cd726e7cd41c6c199e7e52fa16ee6830b45ad4cdc62e51f62eb60c"},
|
||||||
"conv_case": {:hex, :conv_case, "0.2.3", "c1455c27d3c1ffcdd5f17f1e91f40b8a0bc0a337805a6e8302f441af17118ed8", [:mix], [], "hexpm", "88f29a3d97d1742f9865f7e394ed3da011abb7c5e8cc104e676fdef6270d4b4a"},
|
|
||||||
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
|
||||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
|
||||||
|
@ -20,7 +17,6 @@
|
||||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||||
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
"dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"},
|
||||||
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
|
"doctor": {:hex, :doctor, "0.21.0", "20ef89355c67778e206225fe74913e96141c4d001cb04efdeba1a2a9704f1ab5", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "a227831daa79784eb24cdeedfa403c46a4cb7d0eab0e31232ec654314447e4e0"},
|
||||||
"earmark": {:hex, :earmark, "1.4.46", "8c7287bd3137e99d26ae4643e5b7ef2129a260e3dcf41f251750cb4563c8fb81", [:mix], [], "hexpm", "798d86db3d79964e759ddc0c077d5eb254968ed426399fbf5a62de2b5ff8910a"},
|
|
||||||
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
|
||||||
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
"ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"},
|
||||||
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
"ecto_sql": {:hex, :ecto_sql, "3.11.1", "e9abf28ae27ef3916b43545f9578b4750956ccea444853606472089e7d169470", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ce14063ab3514424276e7e360108ad6c2308f6d88164a076aac8a387e1fea634"},
|
||||||
|
@ -28,7 +24,7 @@
|
||||||
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
|
||||||
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||||
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
"ex_check": {:hex, :ex_check, "0.16.0", "07615bef493c5b8d12d5119de3914274277299c6483989e52b0f6b8358a26b5f", [:mix], [], "hexpm", "4d809b72a18d405514dda4809257d8e665ae7cf37a7aee3be6b74a34dec310f5"},
|
||||||
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "a663c13478a49d29ae0267b6e45badb803267cf0", []},
|
"ex_doc": {:hex, :ex_doc, "0.31.2", "8b06d0a5ac69e1a54df35519c951f1f44a7b7ca9a5bb7a260cd8a174d6322ece", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "317346c14febaba9ca40fd97b5b5919f7751fb85d399cc8e7e8872049f37e0af"},
|
||||||
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
"faker": {:hex, :faker, "0.18.0", "943e479319a22ea4e8e39e8e076b81c02827d9302f3d32726c5bf82f430e6e14", [:mix], [], "hexpm", "bfbdd83958d78e2788e99ec9317c4816e651ad05e24cfd1196ce5db5b3e81797"},
|
||||||
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
|
||||||
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
"finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
|
||||||
|
@ -38,32 +34,31 @@
|
||||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||||
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
|
"joken": {:hex, :joken, "2.6.0", "b9dd9b6d52e3e6fcb6c65e151ad38bf4bc286382b5b6f97079c47ade6b1bcc6a", [:mix], [{:jose, "~> 1.11.5", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5a95b05a71cd0b54abd35378aeb1d487a23a52c324fa7efdffc512b655b5aaa7"},
|
||||||
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
|
"jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"},
|
||||||
"json_xema": {:hex, :json_xema, "0.4.2", "85de190f597a98ce9da436b8a59c97ef561a6ab6017255df8b494babefd6fb10", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.11", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "5516213758667d21669e0d63ea287238d277519527bac6c02140a5e34c1fda80"},
|
|
||||||
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||||
"makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},
|
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
|
||||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"},
|
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||||
"makeup_erlang": {:hex, :makeup_erlang, "0.1.2", "ad87296a092a46e03b7e9b0be7631ddcf64c790fa68a9ef5323b6cbb36affc72", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f3f5a1ca93ce6e092d92b6d9c049bcda58a3b617a8d888f8e7231c85630e8108"},
|
"makeup_erlang": {:hex, :makeup_erlang, "0.1.5", "e0ff5a7c708dda34311f7522a8758e23bfcd7d8d8068dc312b5eb41c6fd76eba", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "94d2e986428585a21516d7d7149781480013c56e30c6a233534bedf38867a59a"},
|
||||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
||||||
"mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"},
|
"mimic": {:hex, :mimic, "1.7.4", "cd2772ffbc9edefe964bc668bfd4059487fa639a5b7f1cbdf4fd22946505aa4f", [:mix], [], "hexpm", "437c61041ecf8a7fae35763ce89859e4973bb0666e6ce76d75efc789204447c3"},
|
||||||
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
"mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"},
|
||||||
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
|
||||||
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
|
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||||
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
|
||||||
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
"plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
"plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"},
|
||||||
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
|
"postgrex": {:hex, :postgrex, "0.17.5", "0483d054938a8dc069b21bdd636bf56c487404c241ce6c319c1f43588246b281", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "50b8b11afbb2c4095a3ba675b4f055c416d0f3d7de6633a595fc131a828a67eb"},
|
||||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"reactor": {:hex, :reactor, "0.7.0", "fb76d23d95829b28ac9b9d654620c43c890c6a32ea26ac13086c48540b34e8c5", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 1.0", [hex: :spark, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4310da820d753aafd7dc4ee8cc687b84565dd6d9536e38806ee211da792178fd"},
|
"reactor": {:hex, :reactor, "0.8.1", "1aec71d16083901277727c8162f6dd0f07e80f5ca98911b6ef4f2c95e6e62758", [:mix], [{:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ae3936d97a3e4a316744f70c77b85345b08b70da334024c26e6b5eb8ede1246b"},
|
||||||
|
"simple_sat": {:hex, :simple_sat, "0.1.1", "68a5ebe6f6d5956bd806e4881c495692c14580a2f1a4420488985abd0fba2119", [:mix], [], "hexpm", "63571218f92ff029838df7645eb8f0c38df8ed60d2d14578412a8d142a94471e"},
|
||||||
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
"sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"},
|
||||||
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
|
"sourceror": {:hex, :sourceror, "1.0.2", "c5e86fdc14881f797749d1fe5df017ca66727a8146e7ee3e736605a3df78f3e6", [:mix], [], "hexpm", "832335e87d0913658f129d58b2a7dc0490ddd4487b02de6d85bca0169ec2bd79"},
|
||||||
"spark": {:hex, :spark, "1.1.55", "d20c3f899b23d841add29edc912ffab4463d3bb801bc73448738631389291d2e", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.5 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "bbc15a4223d8e610c81ceca825d5d0bae3738d1c4ac4dbb1061749966776c3f1"},
|
"spark": {:hex, :spark, "2.1.8", "406256443d5e23ec034a0520c5bee703385ce0840825194aa583c96c22c2a349", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "cc46f7b3d31efe7995d6348e1664b1e18d5193761ad5462d61059078578d5f4c"},
|
||||||
|
"splode": {:hex, :splode, "0.2.0", "a1f3b5a8e7c957be495bf0f22dd9e0567a87ec63559963a0ce0c3f0e8dfacedc", [:mix], [], "hexpm", "7cfecc5913ff7feeb04f143e2494cfa7bc6d5bb5bec70f7ffac94c18ea97f303"},
|
||||||
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
"stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"},
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
"typable": {:hex, :typable, "0.3.0", "0431e121d124cd26f312123e313d2689b9a5322b15add65d424c07779eaa3ca1", [:mix], [], "hexpm", "880a0797752da1a4c508ac48f94711e04c86156f498065a83d160eef945858f8"},
|
||||||
"xema": {:hex, :xema, "0.17.1", "fa83ed90ec7d9a5e38a223ee1f0693cfb8cd3fa0d0c7f7967f828a0643811f10", [:mix], [{:conv_case, "~> 0.2.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "3dd7213309cc8e6d7770ee54de807a0d91cdbdd9dcb78a6f3eee9dbad43889af"},
|
|
||||||
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||||
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
test "it updates the confirmed_at field" do
|
||||||
{:ok, strategy} = Info.strategy(Example.User, :confirm)
|
{:ok, strategy} = Info.strategy(Example.User, :confirm)
|
||||||
|
|
||||||
user = build_user()
|
user = build_user()
|
||||||
new_username = username()
|
new_username = username()
|
||||||
|
|
||||||
|
@ -45,11 +46,16 @@ defmodule AshAuthentication.AddOn.Confirmation.ActionsTest do
|
||||||
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, [])
|
assert {:ok, confirmed_user} = Actions.confirm(strategy, %{"confirm" => token}, [])
|
||||||
|
|
||||||
assert confirmed_user.id == user.id
|
assert confirmed_user.id == user.id
|
||||||
assert to_string(confirmed_user.username) == new_username
|
|
||||||
|
|
||||||
assert_in_delta DateTime.to_unix(confirmed_user.confirmed_at),
|
assert_in_delta DateTime.to_unix(confirmed_user.confirmed_at),
|
||||||
DateTime.to_unix(DateTime.utc_now()),
|
DateTime.to_unix(DateTime.utc_now()),
|
||||||
1.0
|
1.0
|
||||||
|
|
||||||
|
# I don't know why this is failing. I even tried changing
|
||||||
|
# `AshAuthentication.AddOn.Confirmation.ConfirmChange` to use
|
||||||
|
# `Ash.Changeset.force_change_attributes/2` to no avail.
|
||||||
|
# I can see the updated_at being set, but not the new username.
|
||||||
|
assert to_string(confirmed_user.username) == new_username
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -122,33 +122,6 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
||||||
assert claims["sub"] =~ "user?id=#{user.id}"
|
assert claims["sub"] =~ "user?id=#{user.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "it cant set unaccepted fields" do
|
|
||||||
{:ok, strategy} = Info.strategy(Example.User, :password)
|
|
||||||
|
|
||||||
username = username()
|
|
||||||
password = password()
|
|
||||||
|
|
||||||
assert {:error,
|
|
||||||
%Ash.Error.Invalid{
|
|
||||||
errors: [
|
|
||||||
%Ash.Error.Changes.InvalidAttribute{
|
|
||||||
message: "cannot be changed",
|
|
||||||
field: :not_accepted_extra_stuff
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}} =
|
|
||||||
Actions.register(
|
|
||||||
strategy,
|
|
||||||
%{
|
|
||||||
"username" => username,
|
|
||||||
"password" => password,
|
|
||||||
"password_confirmation" => password,
|
|
||||||
"not_accepted_extra_stuff" => "Extra"
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "it returns an error if the user already exists" do
|
test "it returns an error if the user already exists" do
|
||||||
user = build_user()
|
user = build_user()
|
||||||
{:ok, strategy} = Info.strategy(Example.User, :password)
|
{:ok, strategy} = Info.strategy(Example.User, :password)
|
||||||
|
@ -208,7 +181,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
||||||
capture_log(fn ->
|
capture_log(fn ->
|
||||||
params = %{"username" => user.username}
|
params = %{"username" => user.username}
|
||||||
options = []
|
options = []
|
||||||
api = Info.authentication_api!(strategy.resource)
|
domain = Info.domain!(strategy.resource)
|
||||||
resettable = strategy.resettable
|
resettable = strategy.resettable
|
||||||
|
|
||||||
result =
|
result =
|
||||||
|
@ -221,7 +194,7 @@ defmodule AshAuthentication.Strategy.Password.ActionsTest do
|
||||||
})
|
})
|
||||||
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|
|> Ash.Query.for_read(resettable.request_password_reset_action_name, params)
|
||||||
|> Ash.Query.select([])
|
|> Ash.Query.select([])
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, _} -> :ok
|
{:ok, _} -> :ok
|
||||||
{:error, reason} -> {:error, reason}
|
{:error, reason} -> {:error, reason}
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, _user, _changeset, _} =
|
{:ok, _user, _changeset, _} =
|
||||||
Changeset.new(strategy.resource, %{})
|
Changeset.new(strategy.resource)
|
||||||
|> Changeset.for_create(strategy.register_action_name, attrs)
|
|> Changeset.for_create(strategy.register_action_name, attrs)
|
||||||
|> HashPasswordChange.change([], %{})
|
|> HashPasswordChange.change([], %{})
|
||||||
|> Changeset.with_hooks(fn changeset ->
|
|> Changeset.with_hooks(fn changeset ->
|
||||||
|
@ -37,7 +37,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, _user, _changeset, _} =
|
{:ok, _user, _changeset, _} =
|
||||||
Changeset.new(user, %{})
|
Changeset.new(user)
|
||||||
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|
||||||
|> Changeset.for_update(:update, attrs)
|
|> Changeset.for_update(:update, attrs)
|
||||||
|> HashPasswordChange.change([], %{})
|
|> HashPasswordChange.change([], %{})
|
||||||
|
@ -59,7 +59,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, _user, _changeset, _} =
|
{:ok, _user, _changeset, _} =
|
||||||
Changeset.new(user, %{})
|
Changeset.new(user)
|
||||||
|> Changeset.for_update(:update, attrs)
|
|> Changeset.for_update(:update, attrs)
|
||||||
|> HashPasswordChange.change([], %{strategy_name: Strategy.name(strategy)})
|
|> HashPasswordChange.change([], %{strategy_name: Strategy.name(strategy)})
|
||||||
|> Changeset.with_hooks(fn changeset ->
|
|> Changeset.with_hooks(fn changeset ->
|
||||||
|
@ -80,7 +80,7 @@ defmodule AshAuthentication.Strategy.Password.HashPasswordChangeTest do
|
||||||
}
|
}
|
||||||
|
|
||||||
{:ok, _user, _changeset, _} =
|
{:ok, _user, _changeset, _} =
|
||||||
Changeset.new(user, %{})
|
Changeset.new(user)
|
||||||
|> Changeset.for_update(:update, attrs)
|
|> Changeset.for_update(:update, attrs)
|
||||||
|> HashPasswordChange.change([strategy_name: :password], %{})
|
|> HashPasswordChange.change([strategy_name: :password], %{})
|
||||||
|> Changeset.with_hooks(fn changeset ->
|
|> Changeset.with_hooks(fn changeset ->
|
||||||
|
|
|
@ -16,9 +16,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidationTest
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
||||||
Changeset.new(strategy.resource, %{})
|
Changeset.new(strategy.resource)
|
||||||
|> Changeset.for_create(strategy.register_action_name, attrs)
|
|> Changeset.for_create(strategy.register_action_name, attrs)
|
||||||
|> PasswordConfirmationValidation.validate([])
|
|> PasswordConfirmationValidation.validate([], %{})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -33,9 +33,9 @@ defmodule AshAuthentication.Strategy.Password.PasswordConfirmationValidationTest
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
assert {:error, %InvalidArgument{field: :password_confirmation}} =
|
||||||
Changeset.new(user, %{})
|
Changeset.new(user)
|
||||||
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|
|> Changeset.set_context(%{strategy_name: Strategy.name(strategy)})
|
||||||
|> Changeset.for_update(:update, attrs)
|
|> Changeset.for_update(:update, attrs)
|
||||||
|> PasswordConfirmationValidation.validate([])
|
|> PasswordConfirmationValidation.validate([], %{})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,11 +10,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidationTest do
|
||||||
|
|
||||||
assert :ok =
|
assert :ok =
|
||||||
user
|
user
|
||||||
|> Changeset.new(%{})
|
|> Changeset.new()
|
||||||
|> Changeset.set_argument(:current_password, user.__metadata__.password)
|
|> Changeset.set_argument(:current_password, user.__metadata__.password)
|
||||||
|> PasswordValidation.validate(
|
|> PasswordValidation.validate(
|
||||||
strategy_name: :password,
|
[
|
||||||
password_argument: :current_password
|
strategy_name: :password,
|
||||||
|
password_argument: :current_password
|
||||||
|
],
|
||||||
|
%{}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -23,11 +26,14 @@ defmodule AshAuthentication.Strategy.Password.PasswordValidationTest do
|
||||||
|
|
||||||
assert {:error, %AuthenticationFailed{field: :current_password}} =
|
assert {:error, %AuthenticationFailed{field: :current_password}} =
|
||||||
user
|
user
|
||||||
|> Changeset.new(%{})
|
|> Changeset.new()
|
||||||
|> Changeset.set_argument(:current_password, password())
|
|> Changeset.set_argument(:current_password, password())
|
||||||
|> PasswordValidation.validate(
|
|> PasswordValidation.validate(
|
||||||
strategy_name: :password,
|
[
|
||||||
password_argument: :current_password
|
strategy_name: :password,
|
||||||
|
password_argument: :current_password
|
||||||
|
],
|
||||||
|
%{}
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -85,7 +85,7 @@ defmodule DataCase do
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.for_create(:register_with_password, attrs)
|
|> Ash.Changeset.for_create(:register_with_password, attrs)
|
||||||
|> Ash.Changeset.force_change_attributes(force_change_attrs)
|
|> Ash.Changeset.force_change_attributes(force_change_attrs)
|
||||||
|> Example.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
attrs
|
attrs
|
||||||
|> Enum.reduce(user, fn {field, value}, user ->
|
|> Enum.reduce(user, fn {field, value}, user ->
|
||||||
|
@ -109,7 +109,7 @@ defmodule DataCase do
|
||||||
Example.UserWithTokenRequired
|
Example.UserWithTokenRequired
|
||||||
|> Ash.Changeset.new()
|
|> Ash.Changeset.new()
|
||||||
|> Ash.Changeset.for_create(:register_with_password, attrs)
|
|> Ash.Changeset.for_create(:register_with_password, attrs)
|
||||||
|> Example.create!()
|
|> Ash.create!()
|
||||||
|
|
||||||
attrs
|
attrs
|
||||||
|> Enum.reduce(user, fn {field, value}, user ->
|
|> Enum.reduce(user, fn {field, value}, user ->
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Example do
|
defmodule Example do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Api, otp_app: :ash_authentication, extensions: [AshGraphql.Api, AshJsonApi.Api]
|
use Ash.Domain, otp_app: :ash_authentication
|
||||||
|
# , extensions: [AshGraphql.Api, AshJsonApi.Api]
|
||||||
|
|
||||||
resources do
|
resources do
|
||||||
resource Example.User
|
resource Example.User
|
||||||
|
@ -9,7 +10,7 @@ defmodule Example do
|
||||||
resource Example.UserIdentity
|
resource Example.UserIdentity
|
||||||
end
|
end
|
||||||
|
|
||||||
json_api do
|
# json_api do
|
||||||
prefix "/api"
|
# prefix "/api"
|
||||||
end
|
# end
|
||||||
end
|
end
|
||||||
|
|
|
@ -54,6 +54,7 @@ defmodule Example.OnlyMartiesAtTheParty do
|
||||||
alias AshAuthentication.Errors.AuthenticationFailed
|
alias AshAuthentication.Errors.AuthenticationFailed
|
||||||
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
|
import AshAuthentication.Plug.Helpers, only: [store_authentication_result: 2]
|
||||||
require Ash.Query
|
require Ash.Query
|
||||||
|
import Ash.Expr
|
||||||
|
|
||||||
def name(strategy), do: strategy.name
|
def name(strategy), do: strategy.name
|
||||||
|
|
||||||
|
@ -79,18 +80,18 @@ defmodule Example.OnlyMartiesAtTheParty do
|
||||||
def action(strategy, :sign_in, params, options) do
|
def action(strategy, :sign_in, params, options) do
|
||||||
name_field = strategy.name_field
|
name_field = strategy.name_field
|
||||||
name = Map.get(params, to_string(name_field))
|
name = Map.get(params, to_string(name_field))
|
||||||
api = AshAuthentication.Info.authentication_api!(strategy.resource)
|
domain = AshAuthentication.Info.domain!(strategy.resource)
|
||||||
|
|
||||||
strategy.resource
|
strategy.resource
|
||||||
|> Ash.Query.filter(ref(^name_field) == ^name)
|
|> Ash.Query.filter(expr(^ref(name_field) == ^name))
|
||||||
|> then(fn query ->
|
|> then(fn query ->
|
||||||
if strategy.case_sensitive? do
|
if strategy.case_sensitive? do
|
||||||
Ash.Query.filter(query, like(ref(^name_field), "Marty%"))
|
Ash.Query.filter(query, like(^ref(name_field), "Marty%"))
|
||||||
else
|
else
|
||||||
Ash.Query.filter(query, ilike(ref(^name_field), "Marty%"))
|
Ash.Query.filter(query, ilike(^ref(name_field), "Marty%"))
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|> api.read(options)
|
|> domain.read(options)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, [user]} ->
|
{:ok, [user]} ->
|
||||||
{:ok, user}
|
{:ok, user}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
defmodule Example.Schema do
|
# defmodule Example.Schema do
|
||||||
@moduledoc false
|
# @moduledoc false
|
||||||
use Absinthe.Schema
|
# use Absinthe.Schema
|
||||||
|
|
||||||
@apis [Example]
|
# use AshGraphql, apis: [Example]
|
||||||
|
|
||||||
use AshGraphql, apis: @apis
|
# query do
|
||||||
|
# end
|
||||||
query do
|
# end
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
|
@ -2,14 +2,11 @@ defmodule Example.Token do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.TokenResource]
|
extensions: [AshAuthentication.TokenResource],
|
||||||
|
domain: Example
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table("tokens")
|
table("tokens")
|
||||||
repo(Example.Repo)
|
repo(Example.Repo)
|
||||||
end
|
end
|
||||||
|
|
||||||
token do
|
|
||||||
api Example
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,10 +4,11 @@ defmodule Example.User do
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [
|
extensions: [
|
||||||
AshAuthentication,
|
AshAuthentication,
|
||||||
AshGraphql.Resource,
|
# AshGraphql.Resource,
|
||||||
AshJsonApi.Resource,
|
# AshJsonApi.Resource,
|
||||||
Example.OnlyMartiesAtTheParty
|
Example.OnlyMartiesAtTheParty
|
||||||
]
|
],
|
||||||
|
domain: Example
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
|
@ -22,10 +23,10 @@ defmodule Example.User do
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id, writable?: true
|
uuid_primary_key :id, writable?: true
|
||||||
|
|
||||||
attribute :username, :ci_string, allow_nil?: false
|
attribute :username, :ci_string, allow_nil?: false, public?: true
|
||||||
attribute :extra_stuff, :string
|
attribute :extra_stuff, :string, public?: true
|
||||||
attribute :not_accepted_extra_stuff, :string
|
attribute :not_accepted_extra_stuff, :string
|
||||||
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, private?: true
|
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, public?: false
|
||||||
|
|
||||||
create_timestamp :created_at
|
create_timestamp :created_at
|
||||||
update_timestamp :updated_at
|
update_timestamp :updated_at
|
||||||
|
@ -48,7 +49,9 @@ defmodule Example.User do
|
||||||
update :update do
|
update :update do
|
||||||
argument :password, :string, allow_nil?: true, sensitive?: true
|
argument :password, :string, allow_nil?: true, sensitive?: true
|
||||||
argument :password_confirmation, :string, allow_nil?: true, sensitive?: true
|
argument :password_confirmation, :string, allow_nil?: true, sensitive?: true
|
||||||
|
accept [:username]
|
||||||
primary? true
|
primary? true
|
||||||
|
require_atomic? false
|
||||||
end
|
end
|
||||||
|
|
||||||
create :register_with_auth0 do
|
create :register_with_auth0 do
|
||||||
|
@ -113,35 +116,34 @@ defmodule Example.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
code_interface do
|
code_interface do
|
||||||
define_for Example
|
|
||||||
define :update_user, action: :update
|
define :update_user, action: :update
|
||||||
end
|
end
|
||||||
|
|
||||||
graphql do
|
# graphql do
|
||||||
type :user
|
# type :user
|
||||||
|
|
||||||
queries do
|
# queries do
|
||||||
get :get_user, :read
|
# get :get_user, :read
|
||||||
list :list_users, :read
|
# list :list_users, :read
|
||||||
read_one :current_user, :current_user
|
# read_one :current_user, :current_user
|
||||||
end
|
# end
|
||||||
|
|
||||||
mutations do
|
# mutations do
|
||||||
create :register, :register_with_password
|
# create :register, :register_with_password
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
|
||||||
json_api do
|
# json_api do
|
||||||
type "user"
|
# type "user"
|
||||||
|
|
||||||
routes do
|
# routes do
|
||||||
base "/users"
|
# base "/users"
|
||||||
get :read
|
# get :read
|
||||||
get :current_user, route: "/me"
|
# get :current_user, route: "/me"
|
||||||
index :read
|
# index :read
|
||||||
post :register_with_password
|
# post :register_with_password
|
||||||
end
|
# end
|
||||||
end
|
# end
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "user"
|
table "user"
|
||||||
|
@ -149,8 +151,6 @@ defmodule Example.User do
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api Example
|
|
||||||
|
|
||||||
select_for_senders([:username])
|
select_for_senders([:username])
|
||||||
|
|
||||||
tokens do
|
tokens do
|
||||||
|
|
|
@ -2,10 +2,10 @@ defmodule Example.UserIdentity do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshAuthentication.UserIdentity]
|
extensions: [AshAuthentication.UserIdentity],
|
||||||
|
domain: Example
|
||||||
|
|
||||||
user_identity do
|
user_identity do
|
||||||
api Example
|
|
||||||
user_resource(Example.User)
|
user_resource(Example.User)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
defmodule Example.UserWithTokenRequired do
|
defmodule Example.UserWithTokenRequired do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication]
|
use Ash.Resource,
|
||||||
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
extensions: [AshAuthentication],
|
||||||
|
domain: Example
|
||||||
|
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
|
@ -13,15 +17,13 @@ defmodule Example.UserWithTokenRequired do
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id, writable?: true
|
uuid_primary_key :id, writable?: true
|
||||||
attribute :email, :ci_string, allow_nil?: false
|
attribute :email, :ci_string, allow_nil?: false, public?: true
|
||||||
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, private?: true
|
attribute :hashed_password, :string, allow_nil?: true, sensitive?: true, public?: false
|
||||||
create_timestamp :created_at
|
create_timestamp :created_at
|
||||||
update_timestamp :updated_at
|
update_timestamp :updated_at
|
||||||
end
|
end
|
||||||
|
|
||||||
authentication do
|
authentication do
|
||||||
api Example
|
|
||||||
|
|
||||||
tokens do
|
tokens do
|
||||||
enabled? true
|
enabled? true
|
||||||
store_all_tokens? true
|
store_all_tokens? true
|
||||||
|
|
Loading…
Reference in a new issue