From bc1e590bcaf3b77dab13af5e35b56bf087ece46d Mon Sep 17 00:00:00 2001 From: James Harton Date: Wed, 8 May 2024 11:50:57 +1200 Subject: [PATCH] docs: Documentation refresh for Ash 3.0. (#665) * docs: Update README to new format. * docs: tidy up docus some more. * docs: More documentation tweaks. * docs: Tweak readme. --- README.md | 99 ++++--- .../DSL:-AshAuthentication.Strategy.Auth0.md | 10 +- .../DSL:-AshAuthentication.Strategy.Github.md | 10 +- .../DSL:-AshAuthentication.Strategy.Google.md | 12 +- ...L:-AshAuthentication.Strategy.MagicLink.md | 2 + .../DSL:-AshAuthentication.Strategy.Oidc.md | 6 + ...SL:-AshAuthentication.Strategy.Password.md | 1 + documentation/dsls/DSL:-AshAuthentication.md | 10 +- documentation/topics/confirmation.md | 7 - documentation/topics/custom-strategy.md | 25 +- documentation/topics/upgrading.md | 14 +- .../{auth0-quickstart.md => auth0.md} | 45 +-- documentation/tutorials/confirmation.md | 173 ++++++++++++ ...-with-authentication.md => get-started.md} | 2 +- .../{github-quickstart.md => github.md} | 8 +- .../{google-quickstart.md => google.md} | 24 +- ...egrating-ash-authentication-and-phoenix.md | 3 - ...gic-links-quickstart.md => magic-links.md} | 3 +- lib/ash_authentication.ex | 10 +- .../add_ons/confirmation/transformer.ex | 3 +- lib/ash_authentication/sender.ex | 12 +- lib/ash_authentication/strategies/auth0.ex | 5 +- .../strategies/auth0/dsl.ex | 5 +- lib/ash_authentication/strategies/custom.ex | 2 +- lib/ash_authentication/strategies/github.ex | 5 +- .../strategies/github/dsl.ex | 5 +- lib/ash_authentication/strategies/google.ex | 6 +- .../strategies/google/dsl.ex | 6 +- .../strategies/magic_link.ex | 2 + lib/ash_authentication/strategies/oidc.ex | 3 + lib/ash_authentication/strategies/oidc/dsl.ex | 3 + .../strategies/password/dsl.ex | 4 +- logos/alembic.png | Bin 0 -> 12067 bytes logos/alembic.svg | 8 + logos/ash-auth-logo.png | Bin 0 -> 30938 bytes logos/ash-auth-logo.svg | 19 ++ logos/ash-auth-small-logo.png | Bin 0 -> 6270 bytes mix.exs | 256 +++++++++--------- 38 files changed, 525 insertions(+), 283 deletions(-) delete mode 100644 documentation/topics/confirmation.md rename documentation/tutorials/{auth0-quickstart.md => auth0.md} (67%) create mode 100644 documentation/tutorials/confirmation.md rename documentation/tutorials/{getting-started-with-authentication.md => get-started.md} (99%) rename documentation/tutorials/{github-quickstart.md => github.md} (96%) rename documentation/tutorials/{google-quickstart.md => google.md} (67%) delete mode 100644 documentation/tutorials/integrating-ash-authentication-and-phoenix.md rename documentation/tutorials/{magic-links-quickstart.md => magic-links.md} (97%) create mode 100644 logos/alembic.png create mode 100644 logos/alembic.svg create mode 100644 logos/ash-auth-logo.png create mode 100644 logos/ash-auth-logo.svg create mode 100644 logos/ash-auth-small-logo.png diff --git a/README.md b/README.md index 6bfbce1..059a6f6 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,79 @@ -# AshAuthentication - -Ash Authentication Logo +![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-black-text.png?raw=true#gh-light-mode-only) +![Logo](https://github.com/ash-project/ash/blob/main/logos/cropped-for-header-white-text.png?raw=true#gh-dark-mode-only) ![Elixir CI](https://github.com/team-alembic/ash_authentication/workflows/Elixir%20Library/badge.svg) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Hex version badge](https://img.shields.io/hexpm/v/ash_authentication.svg)](https://hex.pm/packages/ash_authentication) +[![Hexdocs badge](https://img.shields.io/badge/docs-hexdocs-purple)](https://hexdocs.pm/ash_authentication) -AshAuthentication provides drop-in support for user authentication for users of -the [Ash framework](https://ash-hq.org). It is designed to be highly -configurable, with sensible defaults covering the most common use-cases. +# Ash Authentication -## Installation +Welcome! Here you will find everything you need to know to get started with and use Ash Authentication. This documentation is best viewed on [hexdocs](https://hexdocs.pm/ash_authentication). -The package can be installed by adding `ash_authentication` to your list of -dependencies in `mix.exs`: +## About the Documentation -```elixir -def deps do - [ - {:ash_authentication, "~> 4.0.0-rc.6"} - ] -end -``` +[**Tutorials**](#tutorials) walk you through a series of steps to accomplish a goal. These are **learning-oriented**, and are a great place for beginners to start. -## Documentation +--- -See the [official documentation](https://ash-hq.org/docs/guides/ash_authentication/latest/tutorials/getting-started-with-authentication) for more. +[**Topics**](#topics) provide a high level overview of a specific concept or feature. These are **understanding-oriented**, and are perfect for discovering design patterns, features, and tools related to a given topic. -Additionally, documentation for the latest release will be [available on -hexdocs](https://hexdocs.pm/ash_authentication) and for the [`main` -branch](https://team-alembic.github.io/ash_authentication). +--- -## Contributing +[**How-to**](#how-to) guides are **goal-oriented** recipes for accomplishing specific tasks. These are also good to browse to get an idea of how Ash Authentication works and what is possible with it. -- To contribute updates, fixes or new features please fork and open a - pull-request against `main`. -- To regenerate cheat sheets for the DSLs, run `mix spark.cheat_sheets`. For new strategies ensure you've added them to the extensions and documentation groups in `mix.exs`. -- Please use [conventional - commits](https://www.conventionalcommits.org/en/v1.0.0/) - this allows us to - dynamically generate the changelog. -- Feel free to ask any questions on out [GitHub discussions - page](https://github.com/team-alembic/ash_authentication/discussions). +--- -## Licence +[**Reference**](#reference) documentation is produced automatically from our source code. It comes in the form of module documentation and DSL documentation. This documentation is **information-oriented**. Use the sidebar and the search bar to find relevant reference information. -`AshAuthentication` is licensed under the terms of the [MIT -license](https://opensource.org/licenses/MIT). See the [`LICENSE` file in this -repository](https://github.com/team-alembic/ash_authentication/blob/main/LICENSE) -for details. +## Tutorials + +- [Get Started](documentation/tutorials/get-started.md) + +--- + +## Topics + +- [Custom Strategies](documentation/topics/custom-strategy.md) +- [Policies on Authenticated Resources](documentation/topics/policies-on-authentication-resources.md) +- [Testing](documentation/topics/testing.md) +- [Tokens](documentation/topics/tokens.md) +- [Upgrade guides](documentation/topics/upgrading.md) + +--- + +## How To + +- [Authenticate with Auth0](documentation/how-to/auth0.md) +- [Authenticate with GitHub](documentation/how-to/github.md) +- [Authenticate with Google](documentation/how-to/google.md) +- [Authenticate with Magic Links](documentation/how-to/magic-links.md) +- [Confirmation](documentation/how-to/confirmation.md) + +--- + +## Reference + +- [AshAuthentication DSL](documentation/dsls/DSL:-AshAuthentication.md) +- [AshAuthentication.AddOn.Confirmation DSL](documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md) +- [AshAuthentication.Strategy.Auth0](documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md) +- [AshAuthentication.Strategy.Github DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md) +- [AshAuthentication.Strategy.Google DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md) +- [AshAuthentication.Strategy.MagicLink DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md) +- [AshAuthentication.Strategy.OAuth2 DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md) +- [AshAuthentication.Strategy.Oidc DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md) +- [AshAuthentication.Strategy.Password DSL](documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md) +- [AshAuthentication.TokenResource DSL](documentation/dsls/DSL:-AshAuthentication.TokenResource.md) +- [AshAuthentication.UserIdentity DSL](documentation/dsls/DSL:-AshAuthentication.UserIdentity.md) +- For other reference documentation, see the sidebar & search bar + +## Related packages + +- [Ash Framework](https://hexdocs.pm/ash) +- [Ash Authentication Phoenix](https://hexdocs.pm/ash_authentication_phoenix) | Integrates Ash Authentication into your Phoenix application + +--- + +[![Alembic](logos/alembic.png)](https://alembic.com.au) + +Proudly written and maintained by the team at [Alembic](https://alembic.com.au) for the Ash community. diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md index 530a91a..b236084 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md @@ -15,8 +15,9 @@ In order to use Auth0 you need to provide the following minimum configuration: - `client_secret` - `site` -See the [Auth0 quickstart guide](/documentation/tutorials/auth0-quickstart.md) -for more information. +## More documentation: +- The [Auth0 Tutorial](/documentation/tutorial/auth0.md). +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) @@ -31,8 +32,9 @@ Provides a pre-configured authentication strategy for [Auth0](https://auth0.com/ This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. -For more information see the [Auth0 Quick Start Guide](/documentation/tutorials/auth0-quickstart.md) -in our documentation. +###### More documentation: +- The [Auth0 Tutorial](/documentation/tutorial/auth0.md). +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) ###### Strategy defaults: diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md index b3b4d3c..ff7f015 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md @@ -14,8 +14,9 @@ In order to use GitHub you need to provide the following minimum configuration: - `redirect_uri` - `client_secret` -See the [GitHub quickstart guide](/documentation/tutorials/github-quickstart.html) -for more information. +## More documentation: +- The [GitHub Tutorial](/documentation/tutorial/github.md). +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) @@ -30,8 +31,9 @@ Provides a pre-configured authentication strategy for [GitHub](https://github.co This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. -For more information see the [Github Quick Start Guide](/documentation/tutorials/github-quickstart.md) -in our documentation. +###### More documentation: +- The [GitHub Tutorial](/documentation/tutorial/github.md). +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) ###### Strategy defaults: diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md index 47b1014..913da0b 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md @@ -15,8 +15,10 @@ In order to use Google you need to provide the following minimum configuration: - `client_secret` - `site` -See the [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2) -for Google setup details. +## More documentation: +- The [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2). +- The [Google Tutorial](/documentation/tutorial/google.md) +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) @@ -31,8 +33,10 @@ Provides a pre-configured authentication strategy for [Google](https://google.co This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. -See the [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2) -for Google setup details. +#### More documentation: +- The [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2). +- The [Google Tutorial](/documentation/tutorial/google.md) +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) ###### Strategy defaults: diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md index e68578e..f45e8e0 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md @@ -95,6 +95,8 @@ Dispatching to plugs directly: ...> signed_in_user.id == user.id true +See the [Magic Link Tutorial](/documentation/tutorial/magic-links.md) for more information. + ## authentication.strategies.magic_link diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md index a90e9bf..e3863ad 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md @@ -48,6 +48,9 @@ A random value generator can look like this: AshAuthentication will dynamically generate one for the session if `nonce` is set to `true`. +## More documentation: +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) + ## authentication.strategies.oidc @@ -61,6 +64,9 @@ Provides an OpenID Connect authentication strategy. This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. +###### More documentation: +- The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) + diff --git a/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md b/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md index 5cfb2ca..81493bd 100644 --- a/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md +++ b/documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md @@ -104,6 +104,7 @@ password name \\ :password Strategy for authenticating using local resources as the source of truth. + ### Nested DSLs * [resettable](#authentication-strategies-password-resettable) diff --git a/documentation/dsls/DSL:-AshAuthentication.md b/documentation/dsls/DSL:-AshAuthentication.md index d58dab2..e221d06 100644 --- a/documentation/dsls/DSL:-AshAuthentication.md +++ b/documentation/dsls/DSL:-AshAuthentication.md @@ -59,8 +59,14 @@ Currently supported strategies: - authenticate users against your local database using a unique identity (such as username or email address) and a password. 2. `AshAuthentication.Strategy.OAuth2` - - authenticate using local or remote [OAuth 2.0](https://oauth.net/2/) - compatible services. + - authenticate using local or remote [OAuth 2.0](https://oauth.net/2/) compatible services. + - also includes: + - `AshAuthentication.Strategy.Auth0` + - `AshAuthentication.Strategy.Github` + - `AshAuthentication.Strategy.Google` + - `AshAuthentication.Strategy.Oidc` +3. `AshAuthentication.Strategy.MagicLink` + - authenticate by sending a single-use link to the user. ## Add-ons diff --git a/documentation/topics/confirmation.md b/documentation/topics/confirmation.md deleted file mode 100644 index 1ede706..0000000 --- a/documentation/topics/confirmation.md +++ /dev/null @@ -1,7 +0,0 @@ -# Confirmation - -## Inhibiting Updates - -Inhibiting updates can be done with `d:AshAuthentication.AddOn.Confirmation.**authentication**.add_ons.confirmation.inhibit_updates?`. - -If a change to a monitored field is detected, then the change is stored in the token resource and the changeset updated to not make the requested change. When the token is confirmed, the change will be applied. This could be potentially weird for your users, but useful in the case of a user changing their email address or phone number where you want to verify that the new contact details are reachable. \ No newline at end of file diff --git a/documentation/topics/custom-strategy.md b/documentation/topics/custom-strategy.md index 8d7c74f..c63bd61 100644 --- a/documentation/topics/custom-strategy.md +++ b/documentation/topics/custom-strategy.md @@ -3,6 +3,8 @@ AshAuthentication allows you to bring your own authentication strategy without having to change the Ash Authentication codebase. +> #### Add-on vs Strategy? {:.info} +> > 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 > weird calling "confirmation" an authentication strategy. @@ -93,9 +95,7 @@ here's a brief overview of what each field we've set does: - `target` is the name of the module which defines our entity struct. We've set it to `__MODULE__` which means that we'll have to define the struct on this module. -- `schema` is a keyword list that defines a `NimbleOptions` schema. Spark - provides a number of additional types over the default ones though, so check - out `Spark.OptionsHelpers` for more information. +- `schema` is a keyword list that defines an options schema. See `Spark.Options`. > 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 @@ -148,22 +148,9 @@ 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 concepts: -- "phases" - in terms of HTTP, each strategy is likely to have many phases (eg - OAuth 2.0's "request" and "callback" phases). Essentially you need one - phase for each HTTP endpoint you wish to support with your strategy. In our - case we just want one sign in endpoint. -- "actions" - actions are exactly as they sound - Resource actions which can - be executed by the strategy, whether generated by the strategy (as in the - password strategy) or typed in by the user (as in the OAuth 2.0 strategy). - The reason that we wrap the strategy's actions this way is that all the - built-in strategies (and we hope yours too) allow the user to customise the - name of the actions that it uses. At the very least it should probably - append the strategy name to the action. Using `Strategy.action/4` allows us - to refer these by a more generic name rather than via the user-specified one - (eg `:register` vs `:register_with_password`). -- "routes" - `AshAuthentication.Plug` (or `AshAuthentication.Phoenix.Router`) - will generate routes using `Plug.Router` (or `Phoenix.Router`) - the - `routes/1` callback is used to retrieve this information from the strategy. +- "phases" - in terms of HTTP, each strategy is likely to have many phases (eg OAuth 2.0's "request" and "callback" phases). Essentially you need one phase for each HTTP endpoint you wish to support with your strategy. In our case we just want one sign in endpoint. +- "actions" - actions are exactly as they sound - Resource actions which can be executed by the strategy, whether generated by the strategy (as in the password strategy) or typed in by the user (as in the OAuth 2.0 strategy). The reason that we wrap the strategy's actions this way is that all the built-in strategies (and we hope yours too) allow the user to customise the name of the actions that it uses. At the very least it should probably append the strategy name to the action. Using `Strategy.action/4` allows us to refer these by a more generic name rather than via the user-specified one (eg `:register` vs `:register_with_password`). +- "routes" - `AshAuthentication.Plug` (or [`AshAuthentication.Phoenix.Router.html`](`e:ash_authentication_phoenix:AshAuthentication.Phoenix.Router.html`)) will generate routes using `Plug.Router` (or [`Phoenix.Router`](`e:phoenix:Phoenix.Router.html`)) - the `routes/1` callback is used to retrieve this information from the strategy. Given this information, let's implement the strategy. It's quite long, so I'm going to break it up into smaller chunks. diff --git a/documentation/topics/upgrading.md b/documentation/topics/upgrading.md index 9409bc2..bb1001b 100644 --- a/documentation/topics/upgrading.md +++ b/documentation/topics/upgrading.md @@ -1,9 +1,17 @@ # Upgrading +## Upgrading to version 4.0.0 + +Version 4.0.0 of AshAuthentication adds support for Ash 3.0 and in line with [a number of changes in Ash](`e:ash:upgrading-to-3.0.html`) there are some corresponding changes to Ash Authentication: + +- Token generation is enabled by default, meaning that you will have to explicitly set [`authentication.tokens.enabled?`](documentation/dsls/DSL:-AshAuthentication.md#authentication-tokens-enabled?) to `false` if you don't need them. + +- Sign in tokens are enabled by default in the password strategy. What this means is that instead of returning a regular user token on sign-in in the user's metadata, we generate a short-lived token which can be used to actually sign the user in. This is specifically to allow live-view based sign-in UIs to display an authentication error without requiring a page-load. + ## Upgrading to version 3.6.0. As of version 3.6.0 the `TokenResource` extension adds the `subject` attribute -which allows us to more easily match tokens to specific users. This unlocks +which allows us to more easily match tokens to specific users. This unlocks some new use-cases (eg sign out everywhere). This means that you will need to generate new migrations and migrate your @@ -15,9 +23,9 @@ database. > > If you already have tokens stored in your database then the migration will > likely throw a migration error due to the new `NOT NULL` constraint on -> `subject`. If this happens then you can either delete all your tokens or +> `subject`. If this happens then you can either delete all your tokens or > explicitly add the `subject` attribute to your resource with `allow_nil?` set -> to `true`. eg: +> to `true`. eg: > > ```elixir > attributes do diff --git a/documentation/tutorials/auth0-quickstart.md b/documentation/tutorials/auth0.md similarity index 67% rename from documentation/tutorials/auth0-quickstart.md rename to documentation/tutorials/auth0.md index f66db5c..416a9c2 100644 --- a/documentation/tutorials/auth0-quickstart.md +++ b/documentation/tutorials/auth0.md @@ -1,16 +1,8 @@ -# Auth0 Quick Start Guide +# Auth0 Tutorial -This is a _very quick_ tutorial on how to configure your application to use -Auth0 for authentication. +This is a quick tutorial on how to configure your application to use Auth0 for authentication. -Before you start this tutorial, skip the Token resource while following the -[AshAuthenticationPhoenix guide](https://hexdocs.pm/ash_authentication_phoenix/getting-started-with-ash-authentication-phoenix.html)) - -> [!WARNING] -> Make sure that your `ash_postgres` dependency is `~> 1.3.64`. A bug in previous versions prevents the action shown below from working correctly. - -Next, you need to configure an application in [the Auth0 -dashboard](https://manage.auth0.com/) using the following steps: +First, you need to configure an application in [the Auth0 dashboard](https://manage.auth0.com/) using the following steps: 1. Click "Create Application". 2. Set your application name to something that identifies it. You will likely @@ -18,10 +10,8 @@ dashboard](https://manage.auth0.com/) using the following steps: keep that in mind. 3. Select "Regular Web Application" and click "Create". 4. Switch to the "Settings" tab. -5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll - need them soon. -6. In the "Allowed Callback URLs" section, add your callback URL. The - callback URL is generated from the following information: +5. Copy the "Domain", "Client ID" and "Client Secret" somewhere safe - we'll need them soon. +6. In the "Allowed Callback URLs" section, add your callback URL. The callback URL is generated from the following information: - The base URL of the application - in development that would be `http://localhost:4000/` but in production will be your application's @@ -59,11 +49,7 @@ defmodule MyApp.Accounts.User do end ``` -Because all the configuration values should be kept secret (ie the -`client_secret`) or are likely to be different for each environment we use the -`AshAuthentication.Secret` behaviour to provide them. In this case we're -delegating to the OTP application environment, however you may want to use a -system environment variable or some other secret store (eg Vault). +Because all the configuration values should be kept secret (ie the `client_secret`) or are likely to be different for each environment we use the `AshAuthentication.Secret` behaviour to provide them. In this case we're delegating to the OTP application environment, however you may want to use a system environment variable or some other secret store (eg Vault). ```elixir defmodule MyApp.Secrets do @@ -97,25 +83,16 @@ end The values for this configuration should be: - `client_id` - the client ID copied from the Auth0 settings page. -- `redirect_uri` - the URL to the generated auth routes in your application - (eg `http://localhost:4000/auth`). +- `redirect_uri` - the URL to the generated auth routes in your application (eg `http://localhost:4000/auth`). - `client_secret` the client secret copied from the Auth0 settings page. -- `base_url` - the "domain" value copied from the Auth0 settings page prefixed - with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`). +- `base_url` - the "domain" value copied from the Auth0 settings page prefixed with `https://` (eg `https://dev-yu30yo5y4tg2hg0y.us.auth0.com`). -Lastly, we need to add a register action to your user resource. This is defined -as an upsert so that it can register new users, or update information for -returning users. The default name of the action is `register_with_` followed by -the strategy name. In our case that is `register_with_auth0`. +Lastly, we need to add a register action to your user resource. This is defined as an upsert so that it can register new users, or update information for returning users. The default name of the action is `register_with_` followed by the strategy name. In our case that is `register_with_auth0`. The register action takes two arguments, `user_info` and the `oauth_tokens`. -- `user_info` contains the [`GET /userinfo` response from - Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you - can use to populate your user attributes as needed. -- `oauth_tokens` contains the [`POST /oauth/token` response from - Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want - to store these if you intend to call the Auth0 API on behalf of the user. +- `user_info` contains the [`GET /userinfo` response from Auth0](https://auth0.com/docs/api/authentication#get-user-info) which you can use to populate your user attributes as needed. +- `oauth_tokens` contains the [`POST /oauth/token` response from Auth0](https://auth0.com/docs/api/authentication#get-token) - you may want to store these if you intend to call the Auth0 API on behalf of the user. ```elixir defmodule MyApp.Accounts.User do diff --git a/documentation/tutorials/confirmation.md b/documentation/tutorials/confirmation.md new file mode 100644 index 0000000..c37fe28 --- /dev/null +++ b/documentation/tutorials/confirmation.md @@ -0,0 +1,173 @@ +# Confirmation Tutorial + +This is a quick tutorial on how to configure your application to enable confirmation. + +In this tutorial we'll assume that you have a `User` resource which uses `email` as it's user identifier. We'll show you how to confirm a new user on sign-up and also require them to confirm if they wish to change their email address. + +Here's the user resource we'll be starting with: + +```elixir +defmodule MyApp.Accounts.User do + use Ash.Resource, + extensions: [AshAuthentication], + domain: MyApp.Accounts + + attributes do + uuid_primary_key :id + attribute :email, :ci_string, allow_nil?: false, public?: true, sensitive?: true + attribute :hashed_password, :string, allow_nil?: false, public?: false, sensitive?: true + end + + authentication do + strategies do + password :password do + identity_field :email + hashed_password_field :hashed_password + end + end + end + + identities do + identity :unique_email, [:email] + end +end +``` + +## Confirming newly registered users + +First we start by adding the confirmation add-on to your existing authentication DSL: + +```elixir +defmodule MyApp.Accounts.User do + # ... + + authentication do + # ... + + add_ons do + confirmation :confirm_new_user do + monitor_fields [:email] + confirm_on_create? true + confirm_on_update? false + confirm_action_name :confirm_new_user + sender MyApp.NewUserConfirmationSender + end + end + end +end +``` + +Next we will define our "sender" module using `Swoosh`: + +```elixir +defmodule MyApp.NewUserConfirmationSender do + use AshAuthentication.Sender + + def send(user, token, _opts) do + new() + |> to(user.email) + |> from({"MyApp Admin", "support@myapp.inc"}) + |> subject("Confirm your email address") + |> html_body(""" +

+ Hi!
+ + Someone has tried to register a new account at MyApp. + If it was you, then please click the link below to confirm your identity. If you did not initiate this request then please ignore this email. +

+

+ Click here to confirm your account +

+ """) + |> MyApp.Mailer.deliver() + end +end +``` + +Provided you have your authentication routes hooked up either via `AshAuthentication.Plug` or [`AshAuthentication.Phoenix.Router`](`e:ash_authentication_phoenix:AshAuthentication.Phoenix.Router.html`) then the user will be confirmed when the token is submitted. + +## Confirming changes to monitored fields + +You may want to require a user to perform a confirmation when a certain field changes. For example if a user changes their email address we can send them a new confirmation request. + +First, let's start by defining a new confirmation add-on in our resource: + +```elixir +defmodule MyApp.Accounts.User do + # ... + + authentication do + # ... + + add_ons do + confirmation :confirm_change do + monitor_fields [:email] + confirm_on_create? false + confirm_on_update? true + confirm_action_name :confirm_change + sender MyApp.EmailChangeConfirmationSender + end + end + end +end +``` + +> #### Why two confirmation configurations? {: .info} +> +> While you can perform both of these confirmations with a single confirmation add-on, in general the Ash philosophy is to be more explicit. Each confirmation will have it's own URL (based on the name) and tokens for one will not be able to be used for the other. + +Next, let's define our new sender: + +```elixir +defmodule MyApp.NewUserConfirmationSender do + use AshAuthentication.Sender + + def send(user, token, _opts) do + new() + |> to(user.email) + |> from({"MyApp Admin", "support@myapp.inc"}) + |> subject("Confirm your new email address") + |> html_body(""" +

+ Hi!
+ + You recently changed your email address on MyApp. Please confirm it. +

+

+ Click here to confirm your new email address +

+ """) + |> MyApp.Mailer.deliver() + end +end +``` + +> #### Inhibiting changes {: .tip} +> +> Depending on whether you want the user's changes to be applied _before_ or _after_ confirmation, you can enable the [`inhibit_updates?` DSL option](documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md#authentication-add_ons-confirmation-inhibit_updates?). +> +> When this option is enabled, then any potential changes to monitored fields are instead temporarily stored in the [token resource](documentation/dsls/DSL:-AshAuthentication.TokenResource.md) and applied when the confirmation action is run. + +## Customising the confirmation action + +By default Ash Authentication will generate an update action for confirmation automatically (named `:confirm` unless you change it). You can manually implement this action in order to change it's behaviour and AshAuthentication will validate that the required changes are also present. + +For example, here's an implementation of the `:confirm_change` action mentioned above, which adds a custom change that updates a remote CRM system with the user's new address. + +```elixir +defmodule MyApp.Accounts.User do + # ... + + actions do + # ... + + update :confirm_change do + argument :confirm, :string, allow_nil?: false, public?: true + + change AshAuthentication.AddOn.Confirmation.ConfirmChange + change AshAuthentication.GenerateTokenChange + change MyApp.UpdateCrmSystem, only_when_valid?: true + end + end +end +``` diff --git a/documentation/tutorials/getting-started-with-authentication.md b/documentation/tutorials/get-started.md similarity index 99% rename from documentation/tutorials/getting-started-with-authentication.md rename to documentation/tutorials/get-started.md index 0fb3438..c86ccf3 100644 --- a/documentation/tutorials/getting-started-with-authentication.md +++ b/documentation/tutorials/get-started.md @@ -1,4 +1,4 @@ -# Getting started with Ash Authentication +# Get started with Ash Authentication If you haven't already, read [the getting started guide for Ash](https://ash-hq.org/docs/guides/ash/latest/tutorials/get-started.md). This diff --git a/documentation/tutorials/github-quickstart.md b/documentation/tutorials/github.md similarity index 96% rename from documentation/tutorials/github-quickstart.md rename to documentation/tutorials/github.md index 6edd913..44370a0 100644 --- a/documentation/tutorials/github-quickstart.md +++ b/documentation/tutorials/github.md @@ -1,10 +1,8 @@ -# GitHub Quick Start Guide +# GitHub Tutorial -This is a _very quick_ tutorial on how to configure your application to use -GitHub for authentication. +This is a quick tutorial on how to configure your application to use GitHub for authentication. -First you need to configure an application in your [GitHub developer -settings](https://github.com/settings/developers): +First you need to configure an application in your [GitHub developer settings](https://github.com/settings/developers): 1. Click the "New OAuth App" button. 2. Set your application name to something that identifies it. You will likely diff --git a/documentation/tutorials/google-quickstart.md b/documentation/tutorials/google.md similarity index 67% rename from documentation/tutorials/google-quickstart.md rename to documentation/tutorials/google.md index 879bd80..e6004e1 100644 --- a/documentation/tutorials/google-quickstart.md +++ b/documentation/tutorials/google.md @@ -1,6 +1,6 @@ -# Google Quick Start Guide +# Google Tutorial -This is a _very quick_ tutorial on how to configure Google authentication in your application using the default oauth2 strategy in ash. +This is a quick tutorial on how to configure Google authentication. First you'll need a registered application in [Google Cloud](https://console.cloud.google.com/welcome), in order to get your OAuth 2.0 Client credentials. @@ -23,9 +23,9 @@ defmodule MyApp.Accounts.User do authentication do strategies do oauth2 :google do - client_id "123abc..." - redirect_uri {:ok, "123abc..."} - client_secret fn -> {:ok, "123abc..."} end + client_id MyApp.Secrets + redirect_uri MyApp.Secrets + client_secret MyApp.Secrets end base_url MyApp.Secrets end end @@ -53,19 +53,17 @@ defmodule MyApp.Accounts.User do change AshAuthentication.GenerateTokenChange # Required if you have the `identity_resource` configuration enabled. - # change AshAuthentication.Strategy.OAuth2.IdentityChange + change AshAuthentication.Strategy.OAuth2.IdentityChange - change fn changeset, _ctx -> + change fn changeset, _ -> user_info = Ash.Changeset.get_argument(changeset, :user_info) - changeset - |> Ash.Changeset.change_attribute(:google_info, user_info) - # you could upsert custom user attributes from the given google's user_info - # |> Ash.Changeset.change_attribute(:email, user_info["email"]) - # |> Ash.Changeset.change_attribute(:name, user_info["name"]) - # |> Ash.Changeset.change_attribute(:portrait, user_info["picture"]) + + Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"])) end end end + # ... + end ``` diff --git a/documentation/tutorials/integrating-ash-authentication-and-phoenix.md b/documentation/tutorials/integrating-ash-authentication-and-phoenix.md deleted file mode 100644 index 2fd4d4c..0000000 --- a/documentation/tutorials/integrating-ash-authentication-and-phoenix.md +++ /dev/null @@ -1,3 +0,0 @@ -# Integrating Ash Authentication and Phoenix - -This guide is now located in the [AshAuthenticationPhoenix documentation](https://hexdocs.pm/ash_authentication_phoenix/getting-started-with-ash-authentication-phoenix.html) \ No newline at end of file diff --git a/documentation/tutorials/magic-links-quickstart.md b/documentation/tutorials/magic-links.md similarity index 97% rename from documentation/tutorials/magic-links-quickstart.md rename to documentation/tutorials/magic-links.md index 63ef7df..77ea69a 100644 --- a/documentation/tutorials/magic-links-quickstart.md +++ b/documentation/tutorials/magic-links.md @@ -1,4 +1,4 @@ -# Magic Links Quick Start Guide +# Magic Links Tutorial This is a quick tutorial to get you up and running on Magic Links. This assumes you've set up `ash_authentication` and [password reset](https://ash-hq.org/docs/guides/ash_authentication_phoenix/latest/tutorials/getting-started-with-ash-authentication-phoenix) in your Phoenix project. @@ -75,4 +75,3 @@ end # ... ``` - diff --git a/lib/ash_authentication.ex b/lib/ash_authentication.ex index c1b8eb1..ac6378f 100644 --- a/lib/ash_authentication.ex +++ b/lib/ash_authentication.ex @@ -58,8 +58,14 @@ defmodule AshAuthentication do - authenticate users against your local database using a unique identity (such as username or email address) and a password. 2. `AshAuthentication.Strategy.OAuth2` - - authenticate using local or remote [OAuth 2.0](https://oauth.net/2/) - compatible services. + - authenticate using local or remote [OAuth 2.0](https://oauth.net/2/) compatible services. + - also includes: + - `AshAuthentication.Strategy.Auth0` + - `AshAuthentication.Strategy.Github` + - `AshAuthentication.Strategy.Google` + - `AshAuthentication.Strategy.Oidc` + 3. `AshAuthentication.Strategy.MagicLink` + - authenticate by sending a single-use link to the user. ## Add-ons diff --git a/lib/ash_authentication/add_ons/confirmation/transformer.ex b/lib/ash_authentication/add_ons/confirmation/transformer.ex index 94dd5db..c3d6855 100644 --- a/lib/ash_authentication/add_ons/confirmation/transformer.ex +++ b/lib/ash_authentication/add_ons/confirmation/transformer.ex @@ -117,7 +117,8 @@ defmodule AshAuthentication.AddOn.Confirmation.Transformer do Transformer.build_entity!(Resource.Dsl, [:actions, :update], :argument, name: :confirm, type: Type.String, - allow_nil?: false + allow_nil?: false, + public?: true ) ] diff --git a/lib/ash_authentication/sender.ex b/lib/ash_authentication/sender.ex index d786f08..52960d8 100644 --- a/lib/ash_authentication/sender.ex +++ b/lib/ash_authentication/sender.ex @@ -1,5 +1,5 @@ defmodule AshAuthentication.Sender do - @moduledoc ~S""" + @moduledoc ~S''' A module to implement sending of a token to a user. Allows you to glue sending of instructions to @@ -16,7 +16,7 @@ defmodule AshAuthentication.Sender do ```elixir defmodule MyApp.PasswordResetSender do - use AshAuthentication.PasswordReset.Sender + use AshAuthentication.Sender import Swoosh.Email def send(user, reset_token, _opts) do @@ -24,7 +24,7 @@ defmodule AshAuthentication.Sender do |> to({user.name, user.email}) |> from({"Doc Brown", "emmet@brown.inc"}) |> subject("Password reset instructions") - |> html_body(" + |> html_body("""

Password reset instructions

Hi #{user.name},
@@ -32,10 +32,10 @@ defmodule AshAuthentication.Sender do Someone (maybe you) has requested a password reset for your account. If you did not initiate this request then please ignore this email.

- + Click here to reset - ") + """) |> MyApp.Mailer.deliver() end end @@ -78,7 +78,7 @@ defmodule AshAuthentication.Sender do end end ``` - """ + ''' alias Ash.Resource diff --git a/lib/ash_authentication/strategies/auth0.ex b/lib/ash_authentication/strategies/auth0.ex index 9d5533b..0b710a0 100644 --- a/lib/ash_authentication/strategies/auth0.ex +++ b/lib/ash_authentication/strategies/auth0.ex @@ -14,8 +14,9 @@ defmodule AshAuthentication.Strategy.Auth0 do - `client_secret` - `site` - See the [Auth0 quickstart guide](/documentation/tutorials/auth0-quickstart.md) - for more information. + ## More documentation: + - The [Auth0 Tutorial](/documentation/tutorial/auth0.md). + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) """ alias AshAuthentication.Strategy.{Custom, OAuth2} diff --git a/lib/ash_authentication/strategies/auth0/dsl.ex b/lib/ash_authentication/strategies/auth0/dsl.ex index b17de2b..739bda1 100644 --- a/lib/ash_authentication/strategies/auth0/dsl.ex +++ b/lib/ash_authentication/strategies/auth0/dsl.ex @@ -16,8 +16,9 @@ defmodule AshAuthentication.Strategy.Auth0.Dsl do This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. - For more information see the [Auth0 Quick Start Guide](/documentation/tutorials/auth0-quickstart.md) - in our documentation. + #### More documentation: + - The [Auth0 Tutorial](/documentation/tutorial/auth0.md). + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) #### Strategy defaults: diff --git a/lib/ash_authentication/strategies/custom.ex b/lib/ash_authentication/strategies/custom.ex index bdf4c41..e8e7231 100644 --- a/lib/ash_authentication/strategies/custom.ex +++ b/lib/ash_authentication/strategies/custom.ex @@ -2,7 +2,7 @@ defmodule AshAuthentication.Strategy.Custom do @moduledoc """ Define your own custom authentication strategy. - See [the Custom Strategies guide](/documentation/topics/custom-strategy.html) + See [the Custom Strategies guide](/documentation/topics/custom-strategy.md) for more information. """ diff --git a/lib/ash_authentication/strategies/github.ex b/lib/ash_authentication/strategies/github.ex index af196b8..c103b8e 100644 --- a/lib/ash_authentication/strategies/github.ex +++ b/lib/ash_authentication/strategies/github.ex @@ -13,8 +13,9 @@ defmodule AshAuthentication.Strategy.Github do - `redirect_uri` - `client_secret` - See the [GitHub quickstart guide](/documentation/tutorials/github-quickstart.html) - for more information. + ## More documentation: + - The [GitHub Tutorial](/documentation/tutorial/github.md). + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) """ alias AshAuthentication.Strategy.{Custom, OAuth2} diff --git a/lib/ash_authentication/strategies/github/dsl.ex b/lib/ash_authentication/strategies/github/dsl.ex index b503f5e..15d521f 100644 --- a/lib/ash_authentication/strategies/github/dsl.ex +++ b/lib/ash_authentication/strategies/github/dsl.ex @@ -16,8 +16,9 @@ defmodule AshAuthentication.Strategy.Github.Dsl do This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. - For more information see the [Github Quick Start Guide](/documentation/tutorials/github-quickstart.md) - in our documentation. + #### More documentation: + - The [GitHub Tutorial](/documentation/tutorial/github.md). + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) #### Strategy defaults: diff --git a/lib/ash_authentication/strategies/google.ex b/lib/ash_authentication/strategies/google.ex index 3b398ec..ecd6316 100644 --- a/lib/ash_authentication/strategies/google.ex +++ b/lib/ash_authentication/strategies/google.ex @@ -14,8 +14,10 @@ defmodule AshAuthentication.Strategy.Google do - `client_secret` - `site` - See the [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2) - for Google setup details. + ## More documentation: + - The [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2). + - The [Google Tutorial](/documentation/tutorial/google.md) + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) """ alias AshAuthentication.Strategy.{Custom, OAuth2} diff --git a/lib/ash_authentication/strategies/google/dsl.ex b/lib/ash_authentication/strategies/google/dsl.ex index dd9f984..498ce89 100644 --- a/lib/ash_authentication/strategies/google/dsl.ex +++ b/lib/ash_authentication/strategies/google/dsl.ex @@ -16,8 +16,10 @@ defmodule AshAuthentication.Strategy.Google.Dsl do This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. - See the [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2) - for Google setup details. + ## More documentation: + - The [Google OAuth 2.0 Overview](https://developers.google.com/identity/protocols/oauth2). + - The [Google Tutorial](/documentation/tutorial/google.md) + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) #### Strategy defaults: diff --git a/lib/ash_authentication/strategies/magic_link.ex b/lib/ash_authentication/strategies/magic_link.ex index f1234fa..af9951e 100644 --- a/lib/ash_authentication/strategies/magic_link.ex +++ b/lib/ash_authentication/strategies/magic_link.ex @@ -93,6 +93,8 @@ defmodule AshAuthentication.Strategy.MagicLink do ...> {_conn, {:ok, signed_in_user}} = Plug.Helpers.get_authentication_result(conn) ...> signed_in_user.id == user.id true + + See the [Magic Link Tutorial](/documentation/tutorial/magic-links.md) for more information. """ defstruct identity_field: :username, diff --git a/lib/ash_authentication/strategies/oidc.ex b/lib/ash_authentication/strategies/oidc.ex index 965f5a1..0e1f7a1 100644 --- a/lib/ash_authentication/strategies/oidc.ex +++ b/lib/ash_authentication/strategies/oidc.ex @@ -46,6 +46,9 @@ defmodule AshAuthentication.Strategy.Oidc do AshAuthentication will dynamically generate one for the session if `nonce` is set to `true`. + + ## More documentation: + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) """ alias AshAuthentication.Strategy.{Custom, Oidc} diff --git a/lib/ash_authentication/strategies/oidc/dsl.ex b/lib/ash_authentication/strategies/oidc/dsl.ex index 3166c09..7b89317 100644 --- a/lib/ash_authentication/strategies/oidc/dsl.ex +++ b/lib/ash_authentication/strategies/oidc/dsl.ex @@ -17,6 +17,9 @@ defmodule AshAuthentication.Strategy.Oidc.Dsl do This strategy is built using the `:oauth2` strategy, and thus provides all the same configuration options should you need them. + + #### More documentation: + - The [OAuth2 documentation](`AshAuthentication.Strategy.OAuth2`) """, auto_set_fields: [assent_strategy: Assent.Strategy.OIDC, icon: :oidc], schema: patch_schema() diff --git a/lib/ash_authentication/strategies/password/dsl.ex b/lib/ash_authentication/strategies/password/dsl.ex index 1f99f18..043e3cd 100644 --- a/lib/ash_authentication/strategies/password/dsl.ex +++ b/lib/ash_authentication/strategies/password/dsl.ex @@ -13,7 +13,9 @@ defmodule AshAuthentication.Strategy.Password.Dsl do def dsl do %Entity{ name: :password, - describe: "Strategy for authenticating using local resources as the source of truth.", + describe: """ + Strategy for authenticating using local resources as the source of truth. + """, examples: [ """ password :password do diff --git a/logos/alembic.png b/logos/alembic.png new file mode 100644 index 0000000000000000000000000000000000000000..0be34c47c17c00430e4f91223330031a65db0b61 GIT binary patch literal 12067 zcmX9^1yoaS8^2>T14c+om(rntfTX~rrT<7bNP{$tkdj79x}+Nc$q~}s-6@^YeDi$w zoNe#9=j^?Ad++-^zY0@PlEuZQzy<&S7bYkD0RT{tLqGu&9r;e_gWrYxz;ck&aRva+ z^M5bUe(tsm@+7&-J8c&=dvg~zBPTP!&CQL|%Ff2w#K^&n)85G<^+<#Q0B8Z2w1m2Q z+F`nf_qVBEJ$@q|7KF6oB)svJmJ)<2(uZXYjPX-GMW2cO1iZJ`{f`byx>$FyBs3Iq?#i;t7=uP^@EQakA(Sb!Z&i$A1o>w!rXdlq}=@K zd-A_S8dPlcX@mi1$gnr1D;ab`4DAlEVh5!F2yuM4KJXlJTMx92Aj;t+v*SKjUA6-t z@+zp503bc7A#z9@z-mTV6Xt%C*rZTF2bHnLZCBcD(npb=`IWrnblB;xH>YI{A^;IyNw{gfMP z6eU{tI>+t=cVQp`_eE~08<&V^jqraP<)7<){PVA0qUOJsRMYTZdR|twG31jvuQZM_ z{}ClRZnaHQ$Y~!WeU6iqrnqc_<-$W`AuB@B*%R=Vc<>mFaZx_}(4L{ulfgW}Wb@_gzf(IMe-<$xsgHqkk1O6^pnt`;%cg<1aE?4}YrS~*m*70keIL{RuqEVLUO+F# znRVRX`s3jY(X|`U)lmNol*DBz~8RzM2u&}Auh05~wav8X{{pdH6kJJByI;@*Y zDQO2E82Z|+{l>lhzQNSfumMMbU#G=!B`GgP5s>lw;cp2u%PwPnTYORE`185)r(f7z zGlqtpg!0Q6yQ+$-?eEa)o@aj!pF_{8VQ&+c^Ck_l2gqpRVUJSyAmzZV<8ey2R*H>Z zafBA}f8RklsdDz<34&az$|z}^$HIUTbvvTmhXY{KWpwM4;lx~%mq1_H5riYRE z0krGctwEiVMBYrn`uCvKD2=15IJuorjNzg5jaNAbOQufT@rl zw@Y3yaOYOg^nlIUs++@H+2nnjb~Ju(9%sxzW)L;q(7E~*5ZyfAv_DPQOzKvZ?La_r zzM=E-2mm1u54q}xkO4~#uWE6_tvdxl#(9S40a!xe=8Bcxre@AIntA7BQ7di1{zSwi zR|!L91W(Jn?h1J(5HJo{z|2+Aq0H)jSCE0F&cpVF3cKkhsgzwqgrVz1E4fnAU$jbZ z62#o;I1g%pzenK2maW6<`llHcbOL+8duxK%JKv*3J6thM#XdtBEqhrj@kznL%cHd{y#uTG~4yeyT( zCqDkQwf=gi<|b0K-1sVgdg~^I@#Z!B>w1~21P^1I{{Gn{Rm)9;C3sZtyBi*Cpb5~B zcoO7NzXC6!cPjzHfE}efha{N+3E|U4!luuw^{Tj-PhWTFQKPE?RpShlWJY3H;8Eh8 zs$Gl3xlhaV)TCR+fG8xh15yR&GD>`wyQ2zxy0B zZK)xKp?ZO1CFqj>38^nu$AaM`&#o)oR-JF%r>T2w{)RaEFO zMhvxNJ?uYmk*0OC5uLG6Y#534((;W$0}W!<$Yn(q;jExe1NFbnx7aztDsaA>E_*@V zS~?N>?u9=mS_7t0*@g>!?5A>D>$V>`y>)a4xZ++n2UR2~e;EllF>@)w4n(J&0kvY0 zB@1<2lTmezZg?oOyw~S~zyViUI#XGT0h+^exN%S&IW0WVMR`|LEzND*Y(w9po&oXz z>-#hFHch-zpCrd)TVj8=0^shinPud^*LZjJ^OS@@Rz^OwXL{;bh)-3ZKsL(#h(0Iu zIYTbGqizC&#cUhjly3bG)0=}GseWPC<|DD8z9JJ<@ajTmI#-HAW3wB|HBHb??E?OZ znxRPCn>RbZzZB&9HJo#0zAC{-><5amX|`Rhp&r!?TIAR>T&PH~5mo5UEw16&fE_9s zSIn6_@kTGZMZUjGhdTBIKS__DicWe@d47--w)6m_Iva9{whFw++DJp-dv zue4Q_xZl1@p>*?wawvw$Au^p_`AfacSogOCoWX{1mTA>uW`ivNNsn&t*kq&V4swGM3fHs<8!o*<<5J3ZG#m?R*D}j6@Ipgs}?oMK{Y^+rk62HEP6?FFX zporVVV;SdDvF{hbe?MFB*Q#HC@uPrGiJzd-L(-%r8}NzK1?f9_FKAB!xSP`2kZsS2 zy%q^AP?J0q(wYWg9Ux&pWS#%7oZ)+h;4=&VlA#v zSzCc2IQcshBvN{*rf;S?9H-6kD66Jp>Gx~gh-=#)+LhG@PkteqXRrv~N#~c4YeqGc z!QA#Ejl<@n)Fn}JcU=g{j-bK)G&3vC*zhJ`N!p5#LLaXYWSPq6 zp^&xmWC7F!B=aI(kv}i8h+b*%aZdi8DY?^A7t)!7$rr&qM4~htG_uIfaO3Cce3?-ClmcB3oBEU06j%|;uR(! z8*|f`vR54u{9K-}qcbCAg7QCK(E7PQEli$_Mc(E(IQE;6!(nRWc;!ayo^-)Ly!Z{3 zg2G$eq{H;xU!ob<){7Z)(#`Z46zdYml`=6bB&k35+qjy=B_cLKALp2`rAxjdT$_I% za@d5UoSCeWV8a%ESl$UOfCLjYf_$Trw>5+g+)yMsFuZ?z$ODhx`-9;v?7QoOjfeJM z%It=zY%IPXLnMsxct%W5ND<%A{T&StFd8tjmIj*@1Fz=YZO2I_r@o}!iRSvsF8QzT zy#vwdqjD=6c}CgWFQ=1>k8jgF@ymLOCPC$%@MBN-$-V}QqoSZ$1Kx9m(H&u`-xx6+u>`~GVqTxaD;SSUTF0ANe5{uAP&QG{ zU!s$MTwY+B8K&W7d2?qvp%Dl%^$oq9o1fGB{$#1u7a=b}9QV}G5Gh=LH}?mq)UcAf zR(0#UaF+6=!GaVy-^u$UJ^)WDv)+6Zd+!w$g8P_(^zhF(FcD9K7OqZK37TBTer%v7 zFj@3dP@+4xqrH--dO@-D@8Ga7f??9ndDy+U;-k%YdZ3(Y( zg?c!aqb!^|l{-N4KwmySDsiRFq(#oI<*Fs78U~?+47SU-mNsy;RU zH=ZU(n^fWkD7>FijXMluVZZ{P<(E{)>SDu8FeoHF+h~%5Gwu2|0c{l-LQXm zl6)%_;IKC?se8?`g4Gp639QgHrX;J=IfcM_<-1?C)N_1;dFS6GMgRO^hi1eCxAZvO z=9zf9ZMftx(9!qc-Y9KM)a1z01H?6W&ji07$RF7N5;NijgyaDHgccA#%T>us$V26T znmT<_(J=K3_g^V2v(zseFp)#~o-p)0K5XwP3(s{Sop+vhooX#AIP9{sG35sZGb8#W zf87okbK8WF*596;vo?g#HgVM34v9QO&Qalhg@2^Q%{-Te1p;u7ex20v8^9iqfqU|- zqRwYUL5{r1`j-itD_`Z$)HHqsQYNCQOv*8z#Fd{l8_IfPML4Y;U43m81BshC+6m(_ z-i?x@?=Y$KCiL;hiC;Vb2;rBM=w-#WM}j`Y8itD6 zB^z$e(BIYr8E+tIzZXt^D#$@R#_@z359}xdtPTRe!<|9*?L}}RMgZ=j-2D`LCf~DN@fe?{xZ=(>Wa~dq7kB9Yupselh0k-EtJwY4n z@|s_?@GDvsQ2xPn6jVjWCrJy{;gN_@6V%K(WdBdk7?D0FzM{5#%=!YmN>Un`xgU=l zbtaOxbUC`-0(lXki}%x`h^0D%8kX8c&1%5peH0dmomsg zLc32KjbNLHJ2TV{Z9}4vWIvboM+(WMN9s&Pwl zzdWYQIxw(`v8MXFl25a##as!9Z}^~UY9NJBvu4x(uwmWC`2GBifLoMBZ%_8;;3G`T z!#@%`4JKdq8aeWw#AACQ+i!)G%{rys$nnbU`G1s)pt}qF00E4M3vT+IBn>lWI z?l0Z{o2lK zW>|wtYBpK#&5`TaGtrJDVacPF4@-l$&+9EU2V78|(6MD(+jWKy@oHa2AJKLx6K`Oe z3yCF{cS-EslhWo~O1*+i>J-dezTr(30x7JIAfM#)xGwL)39kiyI-2q*Nym);>fPRg z+8-Zh)CHX$3HLtsNl$nC{01&6v{MZDloVYY?2f3ERhVSJnBIToM7C~5Xs{)o_A*b* z%^&a{W|e`ZExr+kLg#n*kS|`mly1c4uMWsGNh%yGnipc1D>u+l%7YpO(UKkH-5i(q zJP!l0A~L2(2WA3T*Nzc0ll40&-c|a-{szSkru$w;;}$;PDEcMB%9l@q3JMX1u4D-bYVte`R*rec#PKfv$C?{MA2<){x7GG+-!mIW4V2M`e5F-VVA zrpd7``4E!P*2elYX!Mmo6#k{czyD5i=laQno%hlaxGmr|-A3hxLEhXQih)M+s*Spr zhPPWfZ=3Y)m`l6diT^m%b)uXVBN0hp#>Xf|B%ZQIQ8Bw+_)j(FyiV0K2Q9E%vyNyw zg0ZJfnE?=&`+UpiNsLC~JsO`8OvYy*rf0O1sm88HSaMt|3zLX`mEB;^ zWr=0>^p~(cDC^FJ{Z%VBZvwh_f4|>z?<`#p1rI4zPf{MyGHxDcB(psANG(&CX3P)XCMFB2L?F=}pM)NAY&T}>9RP-m zyV-3Tp0=IlPye*X`}V8vrmFwzxo03=-V0|G%w1=sj>`op7P zL0o2b_s{w?iT-0=l#F7B*+5fPDjmVp<^rtt5fqht*owto?oS!3aFUjgpOQeM5F?rU z=hKvUVaPXARut!LZSqpD(oV!cF{tsq1u+_o^1i+Ok>M7{fmqJo(gVeCsq`}>?T{@q zdq0g;@Fd6hx@KWpVY-DTTmbS*e2O#gN8`#;WKIt%_bXnkozqXdQSXS>jD`u+XdYWNCUhH%rxqXN{PAKD54++%uQ`#-(UER_xl zO~7~Wx@cDYZ{7bS-*qIo_)qNiQ>qiHd42(Vpk3zrWBto`Ep<%sMTmI4I=6i5hl(=! zc+TlIu=4X)<}Q-VXcU9KKSCdR#7Idk?6J&Be>oEXvBlwY5#P=2CA3i#6^!NHTN{!G zsNXPl{CPEW=+WLC_b`n?roz;KB}aIk^h`lj;{_GLr&(ZV?@E8w#bXJemKe>ky_cyD z?&aBqmeggs${PzAM`DdW!jl z{$%5oqH&B}5DI8SlVqnZM3}P(2Q^>Uw0|^(NKs*3S|ipL7cPdu(=@hs<@;lFpz&5T zm$A!4<(U}RI{;;dook{^UqsVr^b9*hF@=UXwkqP;jf%)4-?biis|yVW{CY`~ut_~V z%!)bwsI`iv#xbc%xKLnuR~Gh2N&5|VirwFPSMZe+&?L9=Ap67@PBiGvA=hRn74zP& z@j~3on1A!-?&2MfgJ@PMjUl1lhcr8T1?;6l!!SL`GXu8wPH}Y4UgOw2)&E*~BkLH= zg>c^$up6;j;Tk+eL4fv?pJ9Hej7TQo{H&m^A4pvSy)LKFx^qGJY4K7g=%&~fulgE~ z87C(T8X4s{V$ax_nX$ks*=~<`&yck@*`3nAKiHZm0@nDP14NT|geSfN<5hw>w5)et z=0)_APY>Ud$XiB<2JG^j`qHfbsaKgJff)o(vdu_~n zTk+Im7Z1OWdzBU9qr`_{Qg9y4gq?(B+2+my|VxIQn3X&zsh=BMyJ=~aa)R++MjCzq-B)*S`B>3%csOPKcL#_|wme3nq9Pi3 zjy{N|h|BQqQA-eAEEr=E?FknW>=&TcS%+N6GEK>_;1#N!pK(V&iT%i7tp|nUQl$ui zqL^er4YY+C0i$ykCFmGo>g0l|V-;yw7wB>yT_@0l!jDnobLm#^;RymP1HlB6&zF1i z%3tvjLQ%&@EI4#-Km=7s#se`}my-}cMUXv9xiG4Hy*htJDYo|VSh(JM`2v*+?u$`Qd+ig(|N z)`(DG{TLtHzw2A>g|>IVkH+eu?MJP!*yz-_ZFvS%!fmS=!b;y~L6DZu>%AF|V%=Pu zQvZoBVxnN=f>hIb5XBP-`?Bku|UZjmaix%T2F|iNQWCw z015O{)rm=Ud9}%#gXMgBM)FEAh3G4}E0vE=69d(+8RCw4_dSPk@U)); zF3uN}(d*c>ay^CpWXyrjZic7ov&2?ZzLgipk&sBc$6S7;i#*<+YK`(&&t|S-0J4Tp zbi%|8Nt88(4q&M`qORZkU@JynrS`b*X!3w^1CfYD@*eEKt>~F@_qlV+ezL}ROaZBf zCHLiJ*s?IZOChH#Mz44rKaP7yT*$Fqa`iC$Ny-c*&4ubI5x)#0TZ<{&L5R=_%w1}# z6(~vs@_TYs5!8yqn^XID@qJBkkCikAv~*sIK>u_x&3?U-P>nrsj~ z58ZoJ82p>t$bLScIkc=we?E;Mj@fzwmJrr5}+vU72uDUPSf;K z$I)VEOLiiIXi!}Uy(+KU$Q3FGgYg%=9P|i*>8+Dy&8VpzioKQ@>&-^0yhhxVMg1w9 zXb5FU95IRf0IxS7g5-+HW?5+swET1$l<`wb{96Tu}a zyq4I=>Gf&822hn6rRV5Carju^Fr>j}S%S7pp1uwKj+lzw91=_{a+>{4b|IQiu+dLZ zZ&4=^*Aa9v@xZI3cW18E=x+>>b&2!VRmCPXRMg?pxf)kfv4Aw^*D#9Feg2TvH7pva z|F?0dYrau?P*=my4LrKS7?(nXU|ch5+tmYBX=Gb`lNrn9r;T5SNhWk&LCM-QlA%@C zbw&Xl{yG|*OHO(96@#oVPQ?|F2q1``W|avlrlB`9U;9#DJ^_{9vBo!miM|IA1nyguu+Z}yNx+kGj&uy z`Mp`5mqF2N7YjI(h^hGJau7_zM-b8##q(j8IOWK~25?{7-!iLD{?i$zXh9{{uK{nHYn>`rZp1lBem;~9&pRP^gK9) z->ozj`SFckYqe5;1a({8PK?d?%(dtidfx_IG^X22(|c?8Zpx$9&M#e|#b}muS4H@H zmrOr957JEuuXB{evsnQu-%isjC@2udo1qcCb!lu)1*M6q3lm^N5{U^>+#~!6^}yT5 zpzB=6@Q96gc1*@E#ob;?-(4sD*knCh^R;drapBbY&o@jHI}X`~41TqVaB)#p5%3KX z0Z|)RK+Wgp>T@sQ>gYp6m{{%|Bw?p*es;=^_wi0{z&xmzoD2C4Acg(C&bmcCTzFu zvJEwjX(ej;j>cMG#9CK<4PBD~socK?zDfVxH!E4PH3+T8BqEBxZbxvwI}ftH>{R%u z{a@l5SYi+Io^T*?+=Man-okXUr3u4uD#T$ixwe)Dos#<>ZqY~qsU~_A~>uc?KIh~;otm74x$jH zDko<~z6}?NGelcQFML_qM6Oa0z+9}Sw({0x{*G~r4_REg6FE?u3oDgzE&XLBPd7&_ z7hCZn&Oy>m4d^@$x>`EE*G)VrB4KdN<^vc*&M%~^$UK3e%<*s8V3u*k8gki8yzaJQbI*7`#{7AAo3QrQ%2z=K0@0ZY#8P~0o%r%`{g#BTy65OyTp2Rd$ zQ5Q5Fok!B!WA+zyPZNS>Rcp?a01(sSQXseG0Vq!H$pQ~`C8pi2gWZR833o$_4G%hl zxuo7+1qSGZ&wwNb-ssU6tX%b3KOdctmF>E>On~_XL?#>9_2$J&H>a$LRsG!CfnU=0l4f^#8f-PhK2QEnoh|a7X=DQUCf#$ zCu8pJ);v+H8ixI0_CM9y*;d+0kj=@xL(Q0Lcn~tj`#d8SoRWF?vagAEF|3BNzRqmA zelc@y)X7YyGs^1i8ci8mEfMT2O~vmAe|_;E6+ddB10tZa5FItv_dpEP^zv&BW#ps_ z|A58NABL0TOOA$%Rztuz)24U~&DG(x?Tb?dQ)`yAjjPG^bJ@H_6*OOmVk!w>ltz&d3L>ScTa>#+! zrT!%@Bo;7>WU|30=zE`To{iS!3IupeA=5n(fOV>90gzWrKJgBtaUV!E`c7}OH}}iy zU!sKYn!K>0IGW`LGcv^y-cNtWCbNeVa`*aYI^+GiIsKbJf2DSHT~*Ts0JSNwqU2on z{@xYfkl2ROwhm~;{aw#KSLzLmJKE*DX9eKB$Y6;Cy9z(weHzqlU*p}JJ&L<-MLH75 zGc)&~B5nRk;#Xw7OF2D638Xt;+>9wLimB>$uv}y@f9%mVYcEXty1BO8X3tyxyE7Zm zAujzb349Lq8}}&@G9#2Aj;Z8sq9@!;=*#B zD)-p{yrotG{vc`BXG&&PGKii5D!P(`-C}`(VP{vDsdS9l=^~CpN{2MITme#cf_>@y zj6Z1>B_?CCE?ix@92w@o5}Iag0eH(6Lx6SjB!lT&%9%B50EU3;+s zL)My(xS9iM<_~63&Bl58!~8rvpnsCHW8=IZPDeQH57|(M61p5?3^(F=dthxDddLAO zzEC?Zr%2k_vH7#$Y*LH|#%Y__dhkpfxFg}423KP2OPP`rO{*PV2d8uR*1Z9mpUKoq z0)AJ0)0)WOOn-Tth-~v~Fcs|O>Q!~}vPFfJ-3VAEkNajLhj*b}+nNl|cr_FjUFBQy z=RI;E80QLIy@vbF%ml#q>4tR*-fq=B{a}QXxF*TG{ks1|lz8Y8FQ$twq96I7E?@pE zzxtYc$l$Qj?L%aPbP|*uJahpOEZRv_kGEyflpQ4TOAW}-+^}iSE{};d(!wCqa|YO% za`NBKdB(iA;OD(|HTPA9*ZsA6j5h7R1`BaLFw{bvq$l-U?su@|W8|LaH}$Kb8o@E} zx?I4}RazzI52atX?mP1tOjh`G^yZ!e%#f+K$z$Bv<($a++F|m0AftAYzY!Yog|?Bq zR$pD3zWT%PdC%{05SdX?9sBtmh6=z=bYCkG3t5U*_M} zc5IBPM$H=F_ZI@BlW}y#2wH29xX3}fDK^aBdltOf=3{o_YFH|9Ewtamdrb%=L2*A0 z>gJ;fj)M)X-hu@I!)B+TIFWSpDcNi})Nk}K9ne~DY_cs-42u0-s{1|(uJ_C_>S4y@ zrc}oV$+LOjFN5n=$LD26YPAKqc&}|HPv!j8Kwv4`e1MY&{5 zLL!K(b>p7W&Ct{c8i2hP%V;Vbk)Uc z#?AcTZWyVu-R6kgv#P}K=A+YUi#xn`i!AYhC3Ko+>@vRQ3q5Izg`Dhhr5fFbc?4zQ zunkvx(2vFRs%IojH5!43FBPoYQrg9Wb3Thhd4acqa6a_6mM9YiZ_nGg1lL2`9+|*4 zcX+3Y$k^Up-wUs_m?oW?s z$V1gf1m~!IPH<58!%Se6(%74!xy8qKxp`f<+sECy0U*mb0^b*Q@h1LBbY|^x4!1ZV z|M>SyoWpb4cjR#C_ip!i$ra@;vpD~t;`t)d@weyTPI7;PNXq2k zE!fY8DQ1-ELv(PMq^s}v7qZ6TR)x^3SK--~ci{p$z8??WCw!p;<^PkN(!=6fLggx72dcy~n!?}VZy zPM=exZoVGmZK>LdMmioz7dTJE+=(Dd)EK$8+Drz+?0+e-V#WPTMiZTg-o_JPfzzIL-7}R}uCDe<-yEbJ@^WXh7eV`^p-(>elFt_;Ysfo9K zDrzMofyvV)ruOC#@b=BEEAzeO+FnX=0lgmP83|+G3pnc1+ce*Vs5C{|r6IMPG3L^T zSFw0;=c>ar2nYPJi;7bpQ5*W_Vj1fCX)^;6lp&UXVhKHmtBuB_=8u79n#ii)c3Ij* zO2ffY_F9AbLemedR_hGr+}Wc$ugOu1^;KS9h7wS8WOu|qsD5FvFw1AyfhelH1qnbW zHh?P`qiarXedASgkWkaOQKsfz<&vs~F^*>a4j)wZNRNzsYb%lS2_ruFw;;}#c3sSo zsv~@hjsx)qX^q?Tz9pF}xVggviH{ih9UEerSPn9jH631`0x`=M{`frimDZ^(q>}5@ z%y|UPIDBOalYc(EOiL9ms9f>G0HFb{ynR14z6eFj@|q9HjQnDBz!xt7318+$aNS~l+IP&)>_5=)m7{Yrgx}_T!4GP)_I-*RyX`PIt!CjcE4_ZfV}O;F zgvoWKwsJUt{iADyP9S`e|EB@!It?^N9zjC4dt~{hrwo~CmZ-j>gj_>^|1shO3 + + + + + + + diff --git a/logos/ash-auth-logo.png b/logos/ash-auth-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..2115669c28e064305c941bb8b05b53e99706cb6b GIT binary patch literal 30938 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 F{{v%g4R-(l literal 0 HcmV?d00001 diff --git a/logos/ash-auth-logo.svg b/logos/ash-auth-logo.svg new file mode 100644 index 0000000..d60bff0 --- /dev/null +++ b/logos/ash-auth-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/logos/ash-auth-small-logo.png b/logos/ash-auth-small-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6222cefb45a5fbfcf45717058fe918c93f358f50 GIT binary patch literal 6270 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 Wx~$`f1`AyOM%Pf$RjyRD4gMd*wAh{i literal 0 HcmV?d00001 diff --git a/mix.exs b/mix.exs index 97b2ad2..f82abf3 100644 --- a/mix.exs +++ b/mix.exs @@ -2,143 +2,32 @@ defmodule AshAuthentication.MixProject do @moduledoc false use Mix.Project + @description """ + Authentication extension for the Ash Framework. + """ + @version "4.0.0-rc.6" def project do [ app: :ash_authentication, version: @version, - description: "User authentication support for Ash", elixir: "~> 1.13", - start_permanent: Mix.env() == :prod, - preferred_cli_env: [ci: :test], - aliases: aliases(), - deps: deps(), - package: package(), - elixirc_paths: elixirc_paths(Mix.env()), consolidate_protocols: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + elixirc_paths: elixirc_paths(Mix.env()), + package: package(), + deps: deps(), dialyzer: [ plt_add_apps: [:mix, :ex_unit], plt_core_path: "priv/plts", plt_file: {:no_warn, "priv/plts/dialyzer.plt"} ], - docs: [ - main: "readme", - extras: [ - {"README.md", name: "READ ME"}, - "documentation/tutorials/getting-started-with-authentication.md", - "documentation/tutorials/auth0-quickstart.md", - "documentation/tutorials/github-quickstart.md", - "documentation/tutorials/google-quickstart.md", - "documentation/tutorials/integrating-ash-authentication-and-phoenix.md", - "documentation/tutorials/magic-links-quickstart.md", - "documentation/topics/custom-strategy.md", - "documentation/topics/policies-on-authentication-resources.md", - "documentation/topics/testing.md", - "documentation/topics/tokens.md", - "documentation/topics/confirmation.md", - "documentation/topics/upgrading.md", - "documentation/dsls/DSL:-AshAuthentication.md", - "documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md", - "documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md", - "documentation/dsls/DSL:-AshAuthentication.TokenResource.md", - "documentation/dsls/DSL:-AshAuthentication.UserIdentity.md" - ], - groups_for_extras: [ - Tutorials: ~r'documentation/tutorials', - "How To": ~r'documentation/how_to', - Topics: ~r'documentation/topics', - DSLs: ~r'documentation/dsls' - ], - extra_section: "GUIDES", - formatters: ["html"], - before_closing_head_tag: fn type -> - if type == :html do - """ - - """ - end - end, - filter_modules: ~r/^Elixir.AshAuthentication/, - source_url_pattern: - "https://github.com/team-alembic/ash_authentication/blob/main/%{path}#L%{line}", - nest_modules_by_prefix: [ - AshAuthentication.Strategy, - AshAuthentication.AddOn, - AshAuthentication.Plug, - AshAuthentication.Validations - ], - groups_for_modules: [ - Extensions: [ - AshAuthentication, - AshAuthentication.TokenResource, - AshAuthentication.UserIdentity - ], - Strategies: [ - AshAuthentication.Strategy, - AshAuthentication.Strategy.Auth0, - AshAuthentication.Strategy.Github, - AshAuthentication.Strategy.Google, - AshAuthentication.Strategy.MagicLink, - AshAuthentication.Strategy.OAuth2, - AshAuthentication.Strategy.Password - ], - CustomStrategies: [ - ~r/AshAuthentication.Strategy.Custom/ - ], - "Add ons": [ - AshAuthentication.AddOn.Confirmation - ], - Cryptography: [ - AshAuthentication.HashProvider, - AshAuthentication.BcryptProvider, - AshAuthentication.Jwt - ], - Introspection: [ - AshAuthentication.Info, - AshAuthentication.TokenResource.Info, - AshAuthentication.UserIdentity.Info - ], - Utilities: [ - AshAuthentication.Debug, - AshAuthentication.Secret, - AshAuthentication.Sender, - AshAuthentication.Supervisor, - ~r/.*Actions$/, - AshAuthentication.Strategy.Password.Actions, - AshAuthentication.TokenResource.Expunger - ], - Plugs: [~r/^AshAuthentication\.Plug.*/, AshAuthentication.Strategy.MagicLink.Plug], - "Reusable Components": [ - AshAuthentication.GenerateTokenChange, - AshAuthentication.Strategy.Password.HashPasswordChange, - AshAuthentication.Strategy.Password.PasswordConfirmationValidation, - AshAuthentication.Strategy.Password.PasswordValidation, - AshAuthentication.Checks.AshAuthenticationInteraction, - AshAuthentication.Password.Plug, - ~r/AshAuthentication.Validations/ - ], - Errors: ~r/AshAuthentication.Errors/, - "DSL Transformers": [ - ~r/Transformer/, - ~r/Verifier/ - ] - ] - ] + docs: docs(), + aliases: aliases(), + description: @description, + preferred_cli_env: [ci: :test], + consolidate_protocols: Mix.env() == :prod ] end @@ -170,6 +59,125 @@ defmodule AshAuthentication.MixProject do defp extra_applications(:test), do: [:logger, :bcrypt_elixir] defp extra_applications(_), do: [:logger] + defp docs do + [ + main: "readme", + source_ref: "v#{@version}", + logo: "logos/ash-auth-small-logo.png", + extra_section: ["GUIDES"], + extras: [ + {"README.md", name: "Home"}, + "documentation/tutorials/get-started.md", + "documentation/tutorials/auth0.md", + "documentation/tutorials/github.md", + "documentation/tutorials/google.md", + "documentation/tutorials/magic-links.md", + "documentation/tutorials/confirmation.md", + "documentation/topics/custom-strategy.md", + "documentation/topics/policies-on-authentication-resources.md", + "documentation/topics/testing.md", + "documentation/topics/tokens.md", + "documentation/topics/upgrading.md", + "documentation/dsls/DSL:-AshAuthentication.md", + "documentation/dsls/DSL:-AshAuthentication.AddOn.Confirmation.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.Auth0.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.Github.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.Google.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.MagicLink.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.OAuth2.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.Oidc.md", + "documentation/dsls/DSL:-AshAuthentication.Strategy.Password.md", + "documentation/dsls/DSL:-AshAuthentication.TokenResource.md", + "documentation/dsls/DSL:-AshAuthentication.UserIdentity.md", + "CHANGELOG.md" + ], + groups_for_extras: [ + "Start Here": [ + "documentation/home.md", + "documentation/tutorials/get-started.md" + ], + Tutorials: ~r"documentation/tutorials", + Topics: ~r"documentation/topics", + "How To": ~r"documentation/how-to", + Reference: ~r"documentation/dsls" + ], + skip_undefined_reference_warnings_on: [ + "CHANGELOG.md" + ], + nest_modules_by_prefix: [], + before_closing_head_tag: fn type -> + if type == :html do + """ + + """ + end + end, + filter_modules: ~r/^Elixir.AshAuthentication/, + source_url_pattern: + "https://github.com/team-alembic/ash_authentication/blob/main/%{path}#L%{line}", + groups_for_modules: [ + Extensions: [ + AshAuthentication, + AshAuthentication.TokenResource, + AshAuthentication.UserIdentity + ], + Strategies: [ + AshAuthentication.Strategy, + AshAuthentication.AddOn.Confirmation, + AshAuthentication.Strategy.Auth0, + AshAuthentication.Strategy.Custom, + AshAuthentication.Strategy.Github, + AshAuthentication.Strategy.Google, + AshAuthentication.Strategy.MagicLink, + AshAuthentication.Strategy.OAuth2, + AshAuthentication.Strategy.Oidc, + AshAuthentication.Strategy.Password + ], + Cryptography: [ + AshAuthentication.HashProvider, + AshAuthentication.BcryptProvider, + AshAuthentication.Jwt + ], + Introspection: [ + AshAuthentication.Info, + AshAuthentication.TokenResource.Info, + AshAuthentication.UserIdentity.Info + ], + Utilities: [ + AshAuthentication.Debug, + AshAuthentication.Secret, + AshAuthentication.Sender, + AshAuthentication.Supervisor + ], + Plugs: [ + AshAuthentication.Plug, + AshAuthentication.Plug.Helpers + ], + "Reusable Components": [ + AshAuthentication.GenerateTokenChange, + AshAuthentication.Strategy.Password.HashPasswordChange, + AshAuthentication.Strategy.Password.PasswordConfirmationValidation, + AshAuthentication.Strategy.Password.PasswordValidation, + AshAuthentication.Checks.AshAuthenticationInteraction, + AshAuthentication.Password.Plug, + ~r/AshAuthentication.Validations/ + ], + Errors: [ + ~r/^AshAuthentication\.Errors/ + ], + Internals: ~r/.*/ + ] + ] + end + # Run "mix help deps" to learn about dependencies. defp deps do [