From 483e50636179660f6faefabe940ed31805ca0c9f Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Jun 2025 08:39:28 +0200 Subject: [PATCH] chore(AshAuthenticationPhoenix): added library and updated ressources testing password strategy --- .formatter.exs | 2 + .igniter.exs | 10 ++ assets/tailwind.config.js | 1 + config/dev.exs | 5 + lib/accounts/accounts.ex | 10 +- lib/accounts/token.ex | 11 ++ lib/accounts/user.ex | 72 ++++++++++-- .../send_new_user_confirmation_email.ex | 32 ++++++ .../user/senders/send_password_reset_email.ex | 32 ++++++ lib/mv/application.ex | 1 + lib/mv/repo.ex | 2 +- lib/mv_web/auth_overrides.ex | 20 ++++ lib/mv_web/controllers/auth_controller.ex | 55 ++++++++++ lib/mv_web/live_user_auth.ex | 44 ++++++++ lib/mv_web/member_live/index.ex | 2 + lib/mv_web/router.ex | 62 ++++++++++- mix.exs | 6 +- mix.lock | 24 ++-- ...71056_add_accounts_domain_extensions_1.exs | 19 ++++ .../20250602071122_add_accounts_domain.exs | 31 ++++++ priv/resource_snapshots/repo/extensions.json | 3 +- .../repo/tokens/20250602064357.json | 89 +++++++++++++++ .../repo/users/20250602064357.json | 88 +++++++++++++++ .../repo/users/20250602071122.json | 103 ++++++++++++++++++ 24 files changed, 695 insertions(+), 29 deletions(-) create mode 100644 .igniter.exs create mode 100644 lib/accounts/token.ex create mode 100644 lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex create mode 100644 lib/mv/accounts/user/senders/send_password_reset_email.ex create mode 100644 lib/mv_web/auth_overrides.ex create mode 100644 lib/mv_web/controllers/auth_controller.ex create mode 100644 lib/mv_web/live_user_auth.ex create mode 100644 priv/repo/migrations/20250602071056_add_accounts_domain_extensions_1.exs create mode 100644 priv/repo/migrations/20250602071122_add_accounts_domain.exs create mode 100644 priv/resource_snapshots/repo/tokens/20250602064357.json create mode 100644 priv/resource_snapshots/repo/users/20250602064357.json create mode 100644 priv/resource_snapshots/repo/users/20250602071122.json diff --git a/.formatter.exs b/.formatter.exs index 75c5c0c..11132c0 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,7 +1,9 @@ [ import_deps: [ + :ash_authentication_phoenix, :ash_admin, :ash_postgres, + :ash_authentication, :ash_phoenix, :ash, :reactor, diff --git a/.igniter.exs b/.igniter.exs new file mode 100644 index 0000000..bdc3383 --- /dev/null +++ b/.igniter.exs @@ -0,0 +1,10 @@ +# This is a configuration file for igniter. +# For option documentation, see https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html +# To keep it up to date, use `mix igniter.setup` +[ + module_location: :outside_matching_folder, + extensions: [{Igniter.Extensions.Phoenix, []}], + deps_location: :last_list_literal, + source_folders: ["lib", "test/support"], + dont_move_files: [~r"lib/mix"] +] diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index c16fe48..873d6d6 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -7,6 +7,7 @@ const path = require("path") module.exports = { content: [ + "../deps/ash_authentication_phoenix/**/*.*ex", "./js/**/*.js", "../lib/mv_web.ex", "../lib/mv_web/**/*.*ex" diff --git a/config/dev.exs b/config/dev.exs index b7f9ad7..9ef39db 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -84,3 +84,8 @@ config :phoenix_live_view, # Disable swoosh api client as it is only required for production adapters. config :swoosh, :api_client, false + +config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnxOuk1uyAwHz1Q8WB" + +# Signing Secret for Authentication +config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL" diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex index 1bec856..21966ad 100644 --- a/lib/accounts/accounts.ex +++ b/lib/accounts/accounts.ex @@ -4,10 +4,12 @@ defmodule Mv.Accounts do resources do resource Mv.Accounts.User do - define(:create_user, action: :create) - define(:list_users, action: :read) - define(:update_user, action: :update) - define(:destroy_user, action: :destroy) + define :create_user, action: :create + define :list_users, action: :read + define :update_user, action: :update + define :destroy_user, action: :destroy end + + resource Mv.Accounts.Token end end diff --git a/lib/accounts/token.ex b/lib/accounts/token.ex new file mode 100644 index 0000000..723a46b --- /dev/null +++ b/lib/accounts/token.ex @@ -0,0 +1,11 @@ +defmodule Mv.Accounts.Token do + use Ash.Resource, + data_layer: AshPostgres.DataLayer, + extensions: [AshAuthentication.TokenResource], + domain: Mv.Accounts + + postgres do + table "tokens" + repo Mv.Repo + end +end diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 0481b84..f07a57f 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -1,26 +1,78 @@ defmodule Mv.Accounts.User do use Ash.Resource, domain: Mv.Accounts, - data_layer: AshPostgres.DataLayer + data_layer: AshPostgres.DataLayer, + extensions: [AshAuthentication] + + # authorizers: [Ash.Policy.Authorizer] postgres do - table("users") - repo(Mv.Repo) + table "users" + repo Mv.Repo end - attributes do - uuid_primary_key(:id) + authentication do + tokens do + enabled? true + token_resource Mv.Accounts.Token + signing_secret fn _, _ -> + {:ok, Application.get_env(:mv, :token_signing_secret)} + end + end - attribute(:email, :string, allow_nil?: true, public?: true) - attribute(:password_hash, :string, sensitive?: true) - attribute(:oicd_id, :string) + strategies do + password :password do + identity_field :email + hash_provider AshAuthentication.BcryptProvider + confirmation_required? false + end + end end actions do - defaults([:read, :destroy, :create, :update]) + defaults [:read, :create, :destroy, :update] + + read :get_by_subject do + description "Get a user by the subject claim in a JWT" + argument :subject, :string, allow_nil?: false + get? true + prepare AshAuthentication.Preparations.FilterBySubject + end + + # read :sign_in_with_example do + # argument :user_info, :map, allow_nil?: false + # argument :oauth_tokens, :map, allow_nil?: false + # prepare AshAuthentication.Strategy.OAuth2.SignInPreparation + + # filter expr(email == get_path(^arg(:user_info), [:email])) + # end + end + + attributes do + uuid_primary_key :id + + attribute :email, :ci_string, allow_nil?: false, public?: true + attribute :hashed_password, :string, sensitive?: true, allow_nil?: true + attribute :oicd_id, :string, allow_nil?: true end relationships do - belongs_to(:member, Mv.Membership.Member) + belongs_to :member, Mv.Membership.Member end + + identities do + identity :unique_email, [:email] + end + + # You can customize this if you wish, but this is a safe default that + # only allows user data to be interacted with via AshAuthentication. + # policies do + # bypass AshAuthentication.Checks.AshAuthenticationInteraction do + # authorize_if(always()) + # end + + # policy always() do + # forbid_if(always()) + # end + # end end diff --git a/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex new file mode 100644 index 0000000..7fe229c --- /dev/null +++ b/lib/mv/accounts/user/senders/send_new_user_confirmation_email.ex @@ -0,0 +1,32 @@ +defmodule Mv.Accounts.User.Senders.SendNewUserConfirmationEmail do + @moduledoc """ + Sends an email for a new user to confirm their email address. + """ + + use AshAuthentication.Sender + use MvWeb, :verified_routes + + import Swoosh.Email + + alias Mv.Mailer + + @impl true + def send(user, token, _) do + new() + # TODO: Replace with your email + |> from({"noreply", "noreply@example.com"}) + |> to(to_string(user.email)) + |> subject("Confirm your email address") + |> html_body(body(token: token)) + |> Mailer.deliver!() + end + + defp body(params) do + url = url(~p"/confirm_new_user/#{params[:token]}") + + """ +

Click this link to confirm your email:

+

#{url}

+ """ + end +end diff --git a/lib/mv/accounts/user/senders/send_password_reset_email.ex b/lib/mv/accounts/user/senders/send_password_reset_email.ex new file mode 100644 index 0000000..fe1cdb0 --- /dev/null +++ b/lib/mv/accounts/user/senders/send_password_reset_email.ex @@ -0,0 +1,32 @@ +defmodule Mv.Accounts.User.Senders.SendPasswordResetEmail do + @moduledoc """ + Sends a password reset email + """ + + use AshAuthentication.Sender + use MvWeb, :verified_routes + + import Swoosh.Email + + alias Mv.Mailer + + @impl true + def send(user, token, _) do + new() + # TODO: Replace with your email + |> from({"noreply", "noreply@example.com"}) + |> to(to_string(user.email)) + |> subject("Reset your password") + |> html_body(body(token: token)) + |> Mailer.deliver!() + end + + defp body(params) do + url = url(~p"/password-reset/#{params[:token]}") + + """ +

Click this link to reset your password:

+

#{url}

+ """ + end +end diff --git a/lib/mv/application.ex b/lib/mv/application.ex index 2a6eaa3..e0bf462 100644 --- a/lib/mv/application.ex +++ b/lib/mv/application.ex @@ -14,6 +14,7 @@ defmodule Mv.Application do {Phoenix.PubSub, name: Mv.PubSub}, # Start the Finch HTTP client for sending emails {Finch, name: Mv.Finch}, + {AshAuthentication.Supervisor, otp_app: :my}, # Start a worker by calling: Mv.Worker.start_link(arg) # {Mv.Worker, arg}, # Start to serve requests, typically the last entry diff --git a/lib/mv/repo.ex b/lib/mv/repo.ex index 490750e..a8d696a 100644 --- a/lib/mv/repo.ex +++ b/lib/mv/repo.ex @@ -5,7 +5,7 @@ defmodule Mv.Repo do @impl true def installed_extensions do # Add extensions here, and the migration generator will install them. - ["ash-functions"] + ["ash-functions", "citext"] end # Don't open unnecessary transactions diff --git a/lib/mv_web/auth_overrides.ex b/lib/mv_web/auth_overrides.ex new file mode 100644 index 0000000..bec3354 --- /dev/null +++ b/lib/mv_web/auth_overrides.ex @@ -0,0 +1,20 @@ +defmodule MvWeb.AuthOverrides do + use AshAuthentication.Phoenix.Overrides + + # configure your UI overrides here + + # First argument to `override` is the component name you are overriding. + # The body contains any number of configurations you wish to override + # Below are some examples + + # For a complete reference, see https://hexdocs.pm/ash_authentication_phoenix/ui-overrides.html + + # override AshAuthentication.Phoenix.Components.Banner do + # set :image_url, "https://media.giphy.com/media/g7GKcSzwQfugw/giphy.gif" + # set :text_class, "bg-red-500" + # end + + # override AshAuthentication.Phoenix.Components.SignIn do + # set :show_banner, false + # end +end diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex new file mode 100644 index 0000000..913bc4b --- /dev/null +++ b/lib/mv_web/controllers/auth_controller.ex @@ -0,0 +1,55 @@ +defmodule MvWeb.AuthController do + use MvWeb, :controller + use AshAuthentication.Phoenix.Controller + + def success(conn, activity, user, _token) do + return_to = get_session(conn, :return_to) || ~p"/" + + message = + case activity do + {:confirm_new_user, :confirm} -> "Your email address has now been confirmed" + {:password, :reset} -> "Your password has successfully been reset" + _ -> "You are now signed in" + end + + conn + |> delete_session(:return_to) + |> store_in_session(user) + # If your resource has a different name, update the assign name here (i.e :current_admin) + |> assign(:current_user, user) + |> put_flash(:info, message) + |> redirect(to: return_to) + end + + def failure(conn, activity, reason) do + message = + case {activity, reason} do + {_, + %AshAuthentication.Errors.AuthenticationFailed{ + caused_by: %Ash.Error.Forbidden{ + errors: [%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}] + } + }} -> + """ + You have already signed in another way, but have not confirmed your account. + You can confirm your account using the link we sent to you, or by resetting your password. + """ + + _ -> + "Incorrect email or password" + end + + conn + |> put_flash(:error, message) + |> redirect(to: ~p"/sign-in") + end + + def sign_out(conn, _params) do + return_to = get_session(conn, :return_to) || ~p"/" + + conn + |> clear_session() + |> put_flash(:info, "You are now signed out") + |> redirect(to: return_to) + end +end diff --git a/lib/mv_web/live_user_auth.ex b/lib/mv_web/live_user_auth.ex new file mode 100644 index 0000000..d24a683 --- /dev/null +++ b/lib/mv_web/live_user_auth.ex @@ -0,0 +1,44 @@ +defmodule MvWeb.LiveUserAuth do + @moduledoc """ + Helpers for authenticating users in LiveViews. + """ + + import Phoenix.Component + use MvWeb, :verified_routes + + # This is used for nested liveviews to fetch the current user. + # To use, place the following at the top of that liveview: + # on_mount {MvWeb.LiveUserAuth, :current_user} + def on_mount(:current_user, _params, session, socket) do + return_to = session[:return_to] + socket = + socket + |> assign(:return_to, return_to) + |> AshAuthentication.Phoenix.LiveSession.assign_new_resources(session) + {:cont, session, socket} + end + + def on_mount(:live_user_optional, _params, _session, socket) do + if socket.assigns[:current_user] do + {:cont, socket} + else + {:cont, assign(socket, :current_user, nil)} + end + end + + def on_mount(:live_user_required, _params, _session, socket) do + if socket.assigns[:current_user] do + {:cont, socket} + else + {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")} + end + end + + def on_mount(:live_no_user, _params, _session, socket) do + if socket.assigns[:current_user] do + {:halt, Phoenix.LiveView.redirect(socket, to: ~p"/")} + else + {:cont, assign(socket, :current_user, nil)} + end + end +end diff --git a/lib/mv_web/member_live/index.ex b/lib/mv_web/member_live/index.ex index 4e37429..1ab3cad 100644 --- a/lib/mv_web/member_live/index.ex +++ b/lib/mv_web/member_live/index.ex @@ -1,6 +1,8 @@ defmodule MvWeb.MemberLive.Index do use MvWeb, :live_view + on_mount {MvWeb.LiveUserAuth, :live_user_required} + @impl true def render(assigns) do ~H""" diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex index 3e9bdca..f1a841f 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -1,6 +1,10 @@ defmodule MvWeb.Router do use MvWeb, :router + use AshAuthentication.Phoenix.Router + + import AshAuthentication.Plug.Helpers + pipeline :browser do plug :accepts, ["html"] plug :fetch_session @@ -8,21 +12,45 @@ defmodule MvWeb.Router do plug :put_root_layout, html: {MvWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers + plug :load_from_session end pipeline :api do plug :accepts, ["json"] + plug :load_from_bearer + plug :set_actor, :user + end + + scope "/", MvWeb do + pipe_through :browser + + ash_authentication_live_session :authenticated_routes do + # in each liveview, add one of the following at the top of the module: + # + # If an authenticated user must be present: + # on_mount {MvWeb.LiveUserAuth, :live_user_required} + # + # If an authenticated user *may* be present: + # on_mount {MvWeb.LiveUserAuth, :live_user_optional} + # + # If an authenticated user must *not* be present: + # on_mount {MvWeb.LiveUserAuth, :live_no_user} + end end scope "/", MvWeb do pipe_through :browser get "/", PageController, :home - live "/members", MemberLive.Index, :index - live "/members/new", MemberLive.Index, :new - live "/members/:id/edit", MemberLive.Index, :edit - live "/members/:id", MemberLive.Show, :show - live "/members/:id/show/edit", MemberLive.Show, :edit + + ash_authentication_live_session :session_name do + live "/members", MemberLive.Index, :index + live "/members/new", MemberLive.Index, :new + live "/members/:id/edit", MemberLive.Index, :edit + live "/members/:id", MemberLive.Show, :show + live "/members/:id/show/edit", MemberLive.Show, :edit + end + live "/property_types", PropertyTypeLive.Index, :index live "/property_types/new", PropertyTypeLive.Index, :new @@ -35,6 +63,30 @@ defmodule MvWeb.Router do live "/properties/:id/edit", PropertyLive.Index, :edit live "/properties/:id", PropertyLive.Show, :show live "/properties/:id/show/edit", PropertyLive.Show, :edit + auth_routes AuthController, Mv.Accounts.User, path: "/auth" + sign_out_route AuthController + + # Remove these if you'd like to use your own authentication views + sign_in_route register_path: "/register", + reset_path: "/reset", + auth_routes_prefix: "/auth", + on_mount: [{MvWeb.LiveUserAuth, :live_no_user}], + overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] + + # Remove this if you do not want to use the reset password feature + reset_route auth_routes_prefix: "/auth", + overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] + + # Remove this if you do not use the confirmation strategy + confirm_route Mv.Accounts.User, :confirm_new_user, + auth_routes_prefix: "/auth", + overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] + + # Remove this if you do not use the magic link strategy. + # magic_sign_in_route(Mv.Accounts.User, :magic_link, + # auth_routes_prefix: "/auth", + # overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default] + # ) end # Other scopes may use custom stacks. diff --git a/mix.exs b/mix.exs index 687a111..10ad1d7 100644 --- a/mix.exs +++ b/mix.exs @@ -33,6 +33,9 @@ defmodule Mv.MixProject do # Type `mix help deps` for examples and options. defp deps do [ + {:bcrypt_elixir, "~> 3.0"}, + {:ash_authentication, "~> 4.0"}, + {:ash_authentication_phoenix, "~> 2.0"}, {:sourceror, "~> 1.8", only: [:dev, :test]}, {:live_debugger, "~> 0.1", only: [:dev]}, {:ash_admin, "~> 0.13"}, @@ -90,7 +93,8 @@ defmodule Mv.MixProject do "tailwind mv --minify", "esbuild mv --minify", "phx.digest" - ] + ], + "phx.routes": ["phx.routes", "ash_authentication.phoenix.routes"] ] end end diff --git a/mix.lock b/mix.lock index b7190ef..3a5f28a 100644 --- a/mix.lock +++ b/mix.lock @@ -1,18 +1,24 @@ %{ "ash": {:hex, :ash, "3.5.6", "2f187150110b4c280c8551ad411f56d95862fcb37c067a0b8b94eb682bcc43e8", [:mix], [{: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]}, {:igniter, ">= 0.5.24 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, 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.11", [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.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0d9aeb5aacfdc12253fae1e7e4720991868c5f69632c2766afb03b2b1830f55"}, "ash_admin": {:hex, :ash_admin, "0.13.4", "101bc40e299441a65d5c9e911f3801b6ab23eca2e53bb778ed0c6586993cc453", [:mix], [{:ash, ">= 3.4.63 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.1.8 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}], "hexpm", "d546e2f0d87a745c2156c65960f7a7a8b89abd238be5bfabac2176e814846415"}, + "ash_authentication": {:hex, :ash_authentication, "4.8.7", "386c1580f061d54c00e04dc4a8a7b324903460cdc7605ac52a5412eabc22ef33", [:mix], [{:ash, ">= 3.4.29 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, "~> 2.0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "~> 0.2.13", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "1abfec6bedee5cfc7ebafe82d446a9c674ec147ea81c8c95764958b31a9d21a2"}, + "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.7.0", "8447c7fbd1341fdd77460f31e12dba130a82c1f8ee907abc1668ee933edd3c73", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.8", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "25f2392da4169883eb39045f81717387590c01c8adef8cf948ea6a0905f9dacd"}, "ash_phoenix": {:hex, :ash_phoenix, "2.2.0", "aee367f4b3e4c7cfb6a4f1bc219409e0d40961aa9eee5da2113572b66d9f620d", [:mix], [{:ash, ">= 3.4.31 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.4.3 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "ac9d58609b4232094c4789c6bd5f9039d1caa14ca6a893d6ab6ac1aee984e122"}, "ash_postgres": {:hex, :ash_postgres, "2.5.16", "9fc82621aea3c4777f9a322be8cdce10488f0eed50e7d75465285c131c30ec6b", [:mix], [{:ash, ">= 3.4.69 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.68 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "d9f328683ea861707f19e89c16c2c4f3527431c50071b2aea4bac6a822f4f448"}, "ash_sql": {:hex, :ash_sql, "0.2.71", "40cabdd0c7af2eaa0096b2b0eae886085fed1e3b326e20434274120e11dec2c5", [:mix], [{:ash, "~> 3.5", [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]}], "hexpm", "6e22da3d020aecaca9858f430828c12988c3418d252fa39be3f43fde9fd4224d"}, + "assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, 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", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"}, "bandit": {:hex, :bandit, "1.6.8", "be6fcbe01a74e6cba42ae35f4085acaeae9b2d8d360c0908d0b9addbc2811e47", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4fc08c8d4733735d175a007ecb25895e84d09292b0180a2e9f16948182c88b6e"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, - "castore": {:hex, :castore, "1.0.12", "053f0e32700cbec356280c0e835df425a3be4bc1e0627b714330ad9d0f05497f", [:mix], [], "hexpm", "3dca286b2186055ba0c9449b4e95b97bf1b57b47c1f2644555879e659960c224"}, + "castore": {:hex, :castore, "1.0.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, + "comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"}, "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [: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", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 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", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.9.0", "f043eeaca4932ca8e16e5429aebd90f7766f31ac160a25cbd9befe84f2bc068f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b415027f71d5ab57ef2be844b2a10d0c1b5a492d431727f43937adce22ba45ae"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, @@ -22,14 +28,16 @@ "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, - "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, - "igniter": {:hex, :igniter, "0.5.46", "e3ad5b07a194b6e550ddd303bac45a126a65c6157c8acb664b22011cac8e34fd", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:inflex, "~> 2.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "64c59b696b678b2b83e2ee923f5254ac6479aff6c65dd513383bc0e4cdaeeeb7"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "igniter": {:hex, :igniter, "0.6.3", "8bfaf5955ce83301da0f0a53455f73a0bc4dc5aacd6c311363089850a5dc2dd7", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "95d34d8280dea992e05dcbf9865414d69e421a76d743661eaf1d1337ea54fa80"}, "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"}, + "jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "live_debugger": {:hex, :live_debugger, "0.1.5", "e7324a186071ac19885945e4ca7f3257ee07ed8c4ac5862305cab1fd595073aa", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "dc9416960bfe12873bc37707d4669797850f4e8ca4fe192f3195330e1c623634"}, - "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, @@ -38,20 +46,22 @@ "phoenix": {:hex, :phoenix, "1.7.20", "6bababaf27d59f5628f9b608de902a021be2cecefb8231e1dbdc0a2e2e480e9b", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "6be2ab98302e8784a31829e0d50d8bdfa81a23cd912c395bafd8b8bfb5a086c2"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "4.2.1", "35279e2a39140068fc03f8874408d58eef734e488fc142153f055c5454fd1c08", [:mix], [], "hexpm", "cff108100ae2715dd959ae8f2a8cef8e20b593f8dfd031c9cba92702cf23e053"}, + "phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.6", "7b1f0327f54c9eb69845fd09a77accf922f488c549a7e7b8618775eb603a62c7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1681ab813ec26ca6915beb3414aa138f298e17721dc6a2bde9e6eb8a62360ff6"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.5", "f072166f87c44ffaf2b47b65c5ced8c375797830e517bfcf0a006fe7eb113911", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "94abbc84df8a93a64514fc41528695d7326b6f3095e906b32f264ec4280811f3"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"}, - "plug": {:hex, :plug, "1.17.0", "a0832e7af4ae0f4819e0c08dd2e7482364937aea6a8a997a679f2cbb7e026b2e", [: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", "f6692046652a69a00a5a21d0b7e11fcf401064839d59d6b8787f23af55b1e6bc"}, - "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, + "plug": {:hex, :plug, "1.18.0", "d78df36c41f7e798f2edf1f33e1727eae438e9dd5d809a9997c463a108244042", [: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", "819f9e176d51e44dc38132e132fe0accaf6767eab7f0303431e404da8476cfa2"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, "postgrex": {:hex, :postgrex, "0.20.0", "363ed03ab4757f6bc47942eff7720640795eb557e1935951c1626f0d303a3aed", [: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", "d36ef8b36f323d29505314f704e21a1a038e2dc387c6409ee0cd24144e187c0f"}, "reactor": {:hex, :reactor, "0.15.2", "8c1b3fe0527b7a92b0b22c3f33f2e66858dd069bf1dd51d1031f63cd8cbd1fd5", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {: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]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "091435a1fa0cab9bc2ed3934b203a0fd190f62e8b6aca63741f9242b8c7631ac"}, "req": {:hex, :req, "0.5.10", "a3a063eab8b7510785a467f03d30a8d95f66f5c3d9495be3474b61459c54376c", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "8a604815743f8a2d3b5de0659fa3137fa4b1cffd636ecb69b30b2b9b2c2559be"}, "rewrite": {:hex, :rewrite, "1.1.2", "f5a5d10f5fed1491a6ff48e078d4585882695962ccc9e6c779bae025d1f92eda", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "7f8b94b1e3528d0a47b3e8b7bfeca559d2948a65fa7418a9ad7d7712703d39d4"}, + "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "sobelow": {:hex, :sobelow, "0.13.0", "218afe9075904793f5c64b8837cc356e493d88fddde126a463839351870b8d1e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cd6e9026b85fc35d7529da14f95e85a078d9dd1907a9097b3ba6ac7ebbe34a0d"}, - "sourceror": {:hex, :sourceror, "1.9.0", "3bf5fe2d017aaabe3866d8a6da097dd7c331e0d2d54e59e21c2b066d47f1e08e", [:mix], [], "hexpm", "d20a9dd5efe162f0d75a307146faa2e17b823ea4f134f662358d70f0332fed82"}, + "sourceror": {:hex, :sourceror, "1.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, "spark": {:hex, :spark, "2.2.52", "50094275c9bbafa8e5e9eed0ab61983ee209a500e7044914ccf88e9921ae5082", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "f8de8c298bbbf7abd2a80d0ecabcefef65941f397cdbe94ce6165a121b09084f"}, "spitfire": {:hex, :spitfire, "0.2.0", "0de1f519a23f65bde40d316adad53c07a9563f25cc68915d639d8a509a0aad8a", [:mix], [], "hexpm", "743daaee2d81a0d8095431729f478ce49b47ea8943c7d770de86704975cb7775"}, "splode": {:hex, :splode, "0.2.9", "3a2776e187c82f42f5226b33b1220ccbff74f4bcc523dd4039c804caaa3ffdc7", [:mix], [], "hexpm", "8002b00c6e24f8bd1bcced3fbaa5c33346048047bb7e13d2f3ad428babbd95c3"}, diff --git a/priv/repo/migrations/20250602071056_add_accounts_domain_extensions_1.exs b/priv/repo/migrations/20250602071056_add_accounts_domain_extensions_1.exs new file mode 100644 index 0000000..c99c4b8 --- /dev/null +++ b/priv/repo/migrations/20250602071056_add_accounts_domain_extensions_1.exs @@ -0,0 +1,19 @@ +defmodule Mv.Repo.Migrations.AddAccountsDomainExtensions1 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 \"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 \"citext\"") + end +end diff --git a/priv/repo/migrations/20250602071122_add_accounts_domain.exs b/priv/repo/migrations/20250602071122_add_accounts_domain.exs new file mode 100644 index 0000000..30eb877 --- /dev/null +++ b/priv/repo/migrations/20250602071122_add_accounts_domain.exs @@ -0,0 +1,31 @@ +defmodule Mv.Repo.Migrations.AddAccountsDomain 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 + rename table(:users), :password_hash, to: :hashed_password + + alter table(:users) do + modify :email, :citext, null: false + modify :id, :uuid, default: fragment("gen_random_uuid()") + end + + create unique_index(:users, [:email], name: "users_unique_email_index") + end + + def down do + drop_if_exists unique_index(:users, [:email], name: "users_unique_email_index") + + alter table(:users) do + modify :id, :uuid, default: fragment("uuid_generate_v7()") + modify :email, :text, null: true + end + + rename table(:users), :hashed_password, to: :password_hash + end +end diff --git a/priv/resource_snapshots/repo/extensions.json b/priv/resource_snapshots/repo/extensions.json index 33001db..323661b 100644 --- a/priv/resource_snapshots/repo/extensions.json +++ b/priv/resource_snapshots/repo/extensions.json @@ -1,6 +1,7 @@ { "ash_functions_version": 5, "installed": [ - "ash-functions" + "ash-functions", + "citext" ] } \ No newline at end of file diff --git a/priv/resource_snapshots/repo/tokens/20250602064357.json b/priv/resource_snapshots/repo/tokens/20250602064357.json new file mode 100644 index 0000000..680b595 --- /dev/null +++ b/priv/resource_snapshots/repo/tokens/20250602064357.json @@ -0,0 +1,89 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "extra_data", + "type": "map" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "purpose", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "expires_at", + "type": "utc_datetime" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "subject", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "jti", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "FEFA652DA83D7A45390F6667C92DB6E8E8D5CDF709B37834CC4C8AD38E52CFFC", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "tokens" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/users/20250602064357.json b/priv/resource_snapshots/repo/users/20250602064357.json new file mode 100644 index 0000000..6167665 --- /dev/null +++ b/priv/resource_snapshots/repo/users/20250602064357.json @@ -0,0 +1,88 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"uuid_generate_v7()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "email", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "password_hash", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "oicd_id", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "users_member_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "members" + }, + "size": null, + "source": "member_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "48E05AF26A1C25A2D34E5BB3AB26654456D7BD4A73B9C92FC439835D9E453861", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "users" +} \ No newline at end of file diff --git a/priv/resource_snapshots/repo/users/20250602071122.json b/priv/resource_snapshots/repo/users/20250602071122.json new file mode 100644 index 0000000..e0bca27 --- /dev/null +++ b/priv/resource_snapshots/repo/users/20250602071122.json @@ -0,0 +1,103 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "email", + "type": "citext" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "hashed_password", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "oicd_id", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "users_member_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "members" + }, + "size": null, + "source": "member_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "C8B3A4BCBE42E1FFECED346B254902C69E402ADC2528CF4941D342A6E08164FC", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "users_unique_email_index", + "keys": [ + { + "type": "atom", + "value": "email" + } + ], + "name": "unique_email", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "users" +} \ No newline at end of file