From 35a8885267614132c72a204f56bb5fd021e3717a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Thu, 19 Jun 2025 16:17:10 +0200 Subject: [PATCH 01/14] Revert "fix(ci): Dont install dependencies again in test step" This reverts commit d54b226be5b89d5fe65d1b749c95e23396300364. --- .drone.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.drone.yml b/.drone.yml index 1005e64..dc8dcf0 100644 --- a/.drone.yml +++ b/.drone.yml @@ -79,6 +79,8 @@ steps: commands: # Install hex package manager - mix local.hex --force + # Fetch dependencies + - mix deps.get # Run tests - mix test From db3485af66e10e62928a0aab193ba3193998fde1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Eppl=C3=A9e?= Date: Wed, 2 Jul 2025 15:56:00 +0200 Subject: [PATCH 02/14] fix: formatting --- lib/membership/member.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/membership/member.ex b/lib/membership/member.ex index ec2b16f..583f173 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -77,25 +77,21 @@ defmodule Mv.Membership.Member do where: [present(:join_date)], message: "cannot be in the future" - # Exit date not before join date validate compare(:exit_date, greater_than: :join_date), where: [present([:join_date, :exit_date])], message: "cannot be before join date" - # Phone number format (only if set) validate match(:phone_number, ~r/^\+?[0-9\- ]{6,20}$/), where: [present(:phone_number)], message: "is not a valid phone number" - # Postal code format (only if set) validate match(:postal_code, ~r/^\d{5}$/), where: [present(:postal_code)], message: "must consist of 5 digits" - # Email validation with EctoCommons.EmailValidator validate fn changeset, _ -> email = Ash.Changeset.get_attribute(changeset, :email) From f154eea0550776d78078a6764340b0a80ff4c39f Mon Sep 17 00:00:00 2001 From: carla Date: Fri, 30 May 2025 12:20:47 +0200 Subject: [PATCH 03/14] feat(ash): added accounts, user for authentication --- config/config.exs | 2 +- lib/accounts/accounts.ex | 13 +++ lib/accounts/user.ex | 26 ++++++ .../20250530101732_account_migration.exs | 32 +++++++ .../repo/users/20250530101733.json | 88 +++++++++++++++++++ 5 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 lib/accounts/accounts.ex create mode 100644 lib/accounts/user.ex create mode 100644 priv/repo/migrations/20250530101732_account_migration.exs create mode 100644 priv/resource_snapshots/repo/users/20250530101733.json diff --git a/config/config.exs b/config/config.exs index a43af46..43c8cf8 100644 --- a/config/config.exs +++ b/config/config.exs @@ -49,7 +49,7 @@ config :spark, config :mv, ecto_repos: [Mv.Repo], generators: [timestamp_type: :utc_datetime], - ash_domains: [Mv.Membership] + ash_domains: [Mv.Membership, Mv.Accounts] # Configures the endpoint config :mv, MvWeb.Endpoint, diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex new file mode 100644 index 0000000..1bec856 --- /dev/null +++ b/lib/accounts/accounts.ex @@ -0,0 +1,13 @@ +defmodule Mv.Accounts do + use Ash.Domain, + extensions: [AshPhoenix] + + 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) + end + end +end diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex new file mode 100644 index 0000000..0481b84 --- /dev/null +++ b/lib/accounts/user.ex @@ -0,0 +1,26 @@ +defmodule Mv.Accounts.User do + use Ash.Resource, + domain: Mv.Accounts, + data_layer: AshPostgres.DataLayer + + postgres do + table("users") + repo(Mv.Repo) + end + + attributes do + uuid_primary_key(:id) + + attribute(:email, :string, allow_nil?: true, public?: true) + attribute(:password_hash, :string, sensitive?: true) + attribute(:oicd_id, :string) + end + + actions do + defaults([:read, :destroy, :create, :update]) + end + + relationships do + belongs_to(:member, Mv.Membership.Member) + end +end diff --git a/priv/repo/migrations/20250530101732_account_migration.exs b/priv/repo/migrations/20250530101732_account_migration.exs new file mode 100644 index 0000000..7d0f832 --- /dev/null +++ b/priv/repo/migrations/20250530101732_account_migration.exs @@ -0,0 +1,32 @@ +defmodule Mv.Repo.Migrations.AccountMigration 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 + create table(:users, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :email, :text + add :password_hash, :text + add :oicd_id, :text + + add :member_id, + references(:members, + column: :id, + name: "users_member_id_fkey", + type: :uuid, + prefix: "public" + ) + end + end + + def down do + drop constraint(:users, "users_member_id_fkey") + + drop table(:users) + end +end diff --git a/priv/resource_snapshots/repo/users/20250530101733.json b/priv/resource_snapshots/repo/users/20250530101733.json new file mode 100644 index 0000000..d34cb68 --- /dev/null +++ b/priv/resource_snapshots/repo/users/20250530101733.json @@ -0,0 +1,88 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "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": "1CA33A4C9EBDC29717F4B6ADB27E76B42B5BEA7085E47BFBDD9D84E592D44649", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.Mv.Repo", + "schema": null, + "table": "users" +} \ No newline at end of file From 192ceaed45faf978e3e77bc6df6ad73976189e19 Mon Sep 17 00:00:00 2001 From: carla Date: Tue, 3 Jun 2025 08:39:28 +0200 Subject: [PATCH 04/14] 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 | 3 +- mix.lock | 10 +- ...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, 682 insertions(+), 25 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 452ebab..5bd82b5 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 f2cde75..2c82607 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,22 +12,46 @@ 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 plug :set_locale 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 @@ -38,6 +66,30 @@ defmodule MvWeb.Router do live "/properties/:id/show/edit", PropertyLive.Show, :edit post "/set_locale", LocaleController, :set_locale + 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 a1e30ab..7419d61 100644 --- a/mix.exs +++ b/mix.exs @@ -92,7 +92,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 962f445..609684e 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,8 @@ "ecto": {:hex, :ecto, "3.12.6", "8bf762dc5b87d85b7aca7ad5fe31ef8142a84cea473a3381eb933bd925751300", [: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", "4c0cba01795463eebbcd9e4b5ef53c1ee8e68b9c482baef2a80de5a61e7a57fe"}, "ecto_commons": {:hex, :ecto_commons, "0.3.6", "7b1d9e59396cf8c8cbe5a26d50d03f9b6d0fe6c640210dd503622f276f1e59bb", [:mix], [{:burnex, "~> 3.0", [hex: :burnex, repo: "hexpm", optional: true]}, {:ecto, "~> 3.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:ex_phone_number, "~> 0.2", [hex: :ex_phone_number, repo: "hexpm", optional: false]}, {:luhn, "~> 0.3.0", [hex: :luhn, repo: "hexpm", optional: false]}], "hexpm", "3f12981a1e398f206c5d2014e7b732b7ec91b110b9cb84875cb5b28fc75d7a0a"}, "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"}, - "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, + "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"}, "ex_phone_number": {:hex, :ex_phone_number, "0.4.5", "2065cc48c3e9d1ed9821f50877c32f2f6898362cb990f44147ca217c5d1374ed", [:mix], [{:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: false]}], "hexpm", "67163f8706f8cbfef1b1f4b9230c461f19786d0d79fd0b22cbeeefc6f0b99d4a"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, @@ -24,12 +25,14 @@ "floki": {:hex, :floki, "0.37.1", "d7aaee758c8a5b4a7495799a4260754fec5530d95b9c383c03b27359dea117cf", [:mix], [], "hexpm", "673d040cb594d31318d514590246b6dd587ed341d3b67e17c1c0eb8ce7ca6f04"}, "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", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]}, + "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "igniter": {:hex, :igniter, "0.6.7", "4e183afc59d89289e223c4282fd3e9bb39b82e28d0aa6d3369f70fbd3e21a243", [: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", "43b0a584dc84fd1320772c87047355b604ed2bcdd25392b17f7da8bdd09b61ac"}, "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.2.4", "2e0b02874ca562ba2d8cebb9e024c25c0ae9c1f4ee499135a70814e1dea6183e", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20.4 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "bfd0db143be54ccf2872f15bfd2209fbec1083d0b06b81b4cedeecb2fa9ac208"}, "luhn": {:hex, :luhn, "0.3.3", "5aa0c6a32c2db4b9db9f9b883ba8301c1ae169d57199b9e6cb1ba2707bc51d96", [:mix], [], "hexpm", "3e823a913a25aab51352c727f135278d22954874d5f0835be81ed4fec3daf78d"}, @@ -54,7 +57,8 @@ "reactor": {:hex, :reactor, "0.15.4", "ef0c56a901c132529a14ab59fed0ccb4fcecb24308fb189a94c908255d4fdafc", [: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", "783bf62fd0c72ded033afabdb8b6190b7048769771a2a97256e6f0bf4fb0a891"}, "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"}, - "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, + "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.10.0", "38397dedbbc286966ec48c7af13e228b171332be1ad731974438c77791945ce9", [:mix], [], "hexpm", "29dbdfc92e04569c9d8e6efdc422fc1d815f4bd0055dc7c51b8800fb75c4b3f1"}, "spark": {:hex, :spark, "2.2.65", "4c10d109c108417ce394158f330be09ef184878bde45de6462397fbda68cec29", [: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", "d66d5070a77f4c69cb4f007e941ac17d5d751ce71190fcd6e6e5fb42ba86f101"}, "spitfire": {:hex, :spitfire, "0.2.1", "29e154873f05444669c7453d3d931820822cbca5170e88f0f8faa1de74a79b47", [:mix], [], "hexpm", "6eeed75054a38341b2e1814d41bb0a250564092358de2669fdb57ff88141d91b"}, 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 From a6fcaa1640e88bf8c303185b9875a6440b06bc86 Mon Sep 17 00:00:00 2001 From: carla Date: Wed, 18 Jun 2025 09:31:47 +0200 Subject: [PATCH 05/14] feaut(oicd_provider): added oicd provider rauthy and strategy for authentication --- config/dev.exs | 2 + docker-compose.yml | 40 ++- lib/accounts/user.ex | 33 ++- lib/accounts/user_identity.exs | 15 + lib/mv_web/controllers/auth_controller.ex | 2 + .../controllers/page_html/home.html.heex | 262 +++--------------- lib/mv_web/member_live/index.ex | 2 - lib/mv_web/router.ex | 32 ++- 8 files changed, 147 insertions(+), 241 deletions(-) create mode 100644 lib/accounts/user_identity.exs diff --git a/config/dev.exs b/config/dev.exs index 9ef39db..cf6694d 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -89,3 +89,5 @@ config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnx # Signing Secret for Authentication config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL" + +config :mv, :oicd_client_secret , "krkpCYuLtaXUdQDcStaOQRBcfDSRvPdvpmllkraNRStBYMLXgXRlcTxoRkVDrLYv" diff --git a/docker-compose.yml b/docker-compose.yml index 3b4e8ec..03f0366 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,10 @@ +version: "3.5" + +networks: + local: + rauthy-test: + driver: bridge + services: db: image: postgres:17.5-alpine @@ -16,8 +23,37 @@ services: networks: - local -networks: - local: + mailcrab: + image: marlonb/mailcrab:latest + ports: + - "1080:1080" + networks: + - rauthy-test + + + rauthy: + container_name: rauthy-test + image: ghcr.io/sebadob/rauthy:latest + environment: + - LOCAL_TEST=true + - SMTP_URL=mailcrab + - SMTP_PORT=1025 + - SMTP_DANGER_INSECURE=true + - BOOTSTRAP_ADMIN_PASSWORD_PLAIN="RAUTHY" + #- HIQLITE=false + #- PG_HOST=db + #- PG_PORT=5432 + #- PG_USER=postgres + #- PG_PASSWORD=postgres + #- PG_DB_NAME=mv_dev + ports: + - "8080:8080" + depends_on: + - mailcrab + - db + networks: + - rauthy-test + - local volumes: postgres-data: diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index f07a57f..930bc0d 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -21,6 +21,22 @@ defmodule Mv.Accounts.User do end strategies do + oidc :rauthy do + client_id "mv" + base_url "http://localhost:8080/auth/v1" + redirect_uri "http://localhost:4000/auth/user/rauthy/callback" + auth_method :client_secret_jwt + #id_token_signed_response_alg "EdDSA" + #user_url "http://localhost:8080/auth/v1/oidc/userinfo" + #token_url "http://localhost:8080/auth/v1/oidc/token" + #authorize_url "http://localhost:8080/auth/v1/oidc/authorize" + registration_enabled? false + code_verifier true + client_secret fn _, _ -> + Application.fetch_env(:mv, :oicd_client_secret) + end + end + password :password do identity_field :email hash_provider AshAuthentication.BcryptProvider @@ -39,21 +55,23 @@ defmodule Mv.Accounts.User do 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 + read :sign_in_with_rauthy 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 + filter expr(email == get_path(^arg(:user_info), [:email])) + end end + ## TODO: registration ergänzen, seed rausnehmen, oidc_id aus user_info map holen + 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 + attribute :oidc_id, :string, allow_nil?: true end relationships do @@ -62,6 +80,7 @@ defmodule Mv.Accounts.User do identities do identity :unique_email, [:email] + identity :unique_oidc_id, [:oidc_id] end # You can customize this if you wish, but this is a safe default that diff --git a/lib/accounts/user_identity.exs b/lib/accounts/user_identity.exs new file mode 100644 index 0000000..1fe54f8 --- /dev/null +++ b/lib/accounts/user_identity.exs @@ -0,0 +1,15 @@ +defmodule Mv.Accounts.UserIdentity do + use Ash.Resource, + data_layer: AshPostgres.DataLayer, + extensions: [AshAuthentication.UserIdentity], + domain: Mv.Accounts + + user_identity do + user_resource Mv.Accounts.User + end + + postgres do + table "user_identities" + repo Mv.Repo + end +end diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex index 913bc4b..f3dd287 100644 --- a/lib/mv_web/controllers/auth_controller.ex +++ b/lib/mv_web/controllers/auth_controller.ex @@ -22,6 +22,8 @@ defmodule MvWeb.AuthController do end def failure(conn, activity, reason) do + IO.puts(inspect(reason)) + message = case {activity, reason} do {_, diff --git a/lib/mv_web/controllers/page_html/home.html.heex b/lib/mv_web/controllers/page_html/home.html.heex index d72b03c..8cf0506 100644 --- a/lib/mv_web/controllers/page_html/home.html.heex +++ b/lib/mv_web/controllers/page_html/home.html.heex @@ -1,222 +1,52 @@ -<.flash_group flash={@flash} /> - -
-
- -

- Phoenix Framework - - v{Application.spec(:phoenix, :vsn)} - -

-

- Peace of mind from prototype to production. -

-

- Build rich, interactive web applications quickly, with less code and fewer moving parts. Join our growing community of developers using Phoenix to craft APIs, HTML5 apps and more, for fun or at scale. -

-
-
- -
- - - - -
- - - Deploy your application - + + +
+
+
+

+ Demo +

+
+
+
+
+
+
+
+
+
diff --git a/lib/mv_web/member_live/index.ex b/lib/mv_web/member_live/index.ex index 5bd82b5..452ebab 100644 --- a/lib/mv_web/member_live/index.ex +++ b/lib/mv_web/member_live/index.ex @@ -1,8 +1,6 @@ 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 2c82607..e4be8e1 100644 --- a/lib/mv_web/router.ex +++ b/lib/mv_web/router.ex @@ -42,30 +42,34 @@ defmodule MvWeb.Router do scope "/", MvWeb do pipe_through :browser - get "/", PageController, :home + ash_authentication_live_session :authentication_required, + on_mount: {MvWeb.LiveUserAuth, :live_user_required} do + + get "/", PageController, :home - 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 + live "/property_types/:id/edit", PropertyTypeLive.Index, :edit + live "/property_types/:id", PropertyTypeLive.Show, :show + live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit - live "/property_types", PropertyTypeLive.Index, :index - live "/property_types/new", PropertyTypeLive.Index, :new - live "/property_types/:id/edit", PropertyTypeLive.Index, :edit - live "/property_types/:id", PropertyTypeLive.Show, :show - live "/property_types/:id/show/edit", PropertyTypeLive.Show, :edit - - live "/properties", PropertyLive.Index, :index - live "/properties/new", PropertyLive.Index, :new - live "/properties/:id/edit", PropertyLive.Index, :edit - live "/properties/:id", PropertyLive.Show, :show - live "/properties/:id/show/edit", PropertyLive.Show, :edit + live "/properties", PropertyLive.Index, :index + live "/properties/new", PropertyLive.Index, :new + live "/properties/:id/edit", PropertyLive.Index, :edit + live "/properties/:id", PropertyLive.Show, :show + live "/properties/:id/show/edit", PropertyLive.Show, :edit post "/set_locale", LocaleController, :set_locale + + end + + # ASHAUTHENTICATION GENERATED AUTH ROUTES auth_routes AuthController, Mv.Accounts.User, path: "/auth" sign_out_route AuthController From 7bfde5e23017b7b56ba66e046416c6e266cca4f3 Mon Sep 17 00:00:00 2001 From: carla Date: Thu, 19 Jun 2025 15:34:24 +0200 Subject: [PATCH 06/14] doc: added comments and updated to latest ashautentication version and required changes --- config/dev.exs | 4 +- docker-compose.yml | 2 +- lib/accounts/accounts.ex | 3 + lib/accounts/token.ex | 3 + lib/accounts/user.ex | 49 ++++++--- lib/accounts/user_identity.exs | 11 +- lib/mv_web/controllers/auth_controller.ex | 4 +- .../controllers/page_html/home.html.heex | 5 +- lib/mv_web/live_user_auth.ex | 2 + lib/mv_web/router.ex | 4 +- mix.exs | 3 + mix.lock | 12 +- .../20250530101732_account_migration.exs | 32 ------ ...71056_add_accounts_domain_extensions_1.exs | 19 ---- .../20250602071122_add_accounts_domain.exs | 31 ------ .../repo/tokens/20250602064357.json | 89 --------------- .../repo/users/20250530101733.json | 88 --------------- .../repo/users/20250602064357.json | 88 --------------- .../repo/users/20250602071122.json | 103 ------------------ 19 files changed, 74 insertions(+), 478 deletions(-) delete mode 100644 priv/repo/migrations/20250530101732_account_migration.exs delete mode 100644 priv/repo/migrations/20250602071056_add_accounts_domain_extensions_1.exs delete mode 100644 priv/repo/migrations/20250602071122_add_accounts_domain.exs delete mode 100644 priv/resource_snapshots/repo/tokens/20250602064357.json delete mode 100644 priv/resource_snapshots/repo/users/20250530101733.json delete mode 100644 priv/resource_snapshots/repo/users/20250602064357.json delete mode 100644 priv/resource_snapshots/repo/users/20250602071122.json diff --git a/config/dev.exs b/config/dev.exs index cf6694d..7b4df11 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -90,4 +90,6 @@ config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnx # Signing Secret for Authentication config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL" -config :mv, :oicd_client_secret , "krkpCYuLtaXUdQDcStaOQRBcfDSRvPdvpmllkraNRStBYMLXgXRlcTxoRkVDrLYv" +config :mv, + :oicd_client_secret, + "auhoZABKjohxhmeVCIDzMMUkBOtDQjPKiQiFQwmIogfaPPvBOeqtvnEJuTYIWcIc" diff --git a/docker-compose.yml b/docker-compose.yml index 03f0366..7fed5d7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,7 +39,7 @@ services: - SMTP_URL=mailcrab - SMTP_PORT=1025 - SMTP_DANGER_INSECURE=true - - BOOTSTRAP_ADMIN_PASSWORD_PLAIN="RAUTHY" + - BOOTSTRAP_ADMIN_PASSWORD_PLAIN=RauthyTest12345 #- HIQLITE=false #- PG_HOST=db #- PG_PORT=5432 diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex index 21966ad..55e8a4b 100644 --- a/lib/accounts/accounts.ex +++ b/lib/accounts/accounts.ex @@ -1,4 +1,7 @@ defmodule Mv.Accounts do + @moduledoc """ + AshAuthentication specific domain to handle Authentication for users. + """ use Ash.Domain, extensions: [AshPhoenix] diff --git a/lib/accounts/token.ex b/lib/accounts/token.ex index 723a46b..ab9c3a7 100644 --- a/lib/accounts/token.ex +++ b/lib/accounts/token.ex @@ -1,4 +1,7 @@ defmodule Mv.Accounts.Token do + @moduledoc """ + AshAuthentication specific ressource + """ use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication.TokenResource], diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index 930bc0d..a7191a8 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -1,4 +1,7 @@ defmodule Mv.Accounts.User do + @moduledoc """ + The ressource for keeping user-specific data related to the login process. It is used by AshAuthentication to handle the Authentication strategies like SSO. + """ use Ash.Resource, domain: Mv.Accounts, data_layer: AshPostgres.DataLayer, @@ -11,10 +14,17 @@ defmodule Mv.Accounts.User do repo Mv.Repo end + @doc """ + AshAuthentication specific: Defines the strategies we want to use for authentication. + Currently password and SSO with Rauthy as OIDC provider + """ authentication do tokens do enabled? true token_resource Mv.Accounts.Token + require_token_presence_for_authentication? true + store_all_tokens? true + signing_secret fn _, _ -> {:ok, Application.get_env(:mv, :token_signing_secret)} end @@ -22,18 +32,14 @@ defmodule Mv.Accounts.User do strategies do oidc :rauthy do - client_id "mv" - base_url "http://localhost:8080/auth/v1" - redirect_uri "http://localhost:4000/auth/user/rauthy/callback" - auth_method :client_secret_jwt - #id_token_signed_response_alg "EdDSA" - #user_url "http://localhost:8080/auth/v1/oidc/userinfo" - #token_url "http://localhost:8080/auth/v1/oidc/token" - #authorize_url "http://localhost:8080/auth/v1/oidc/authorize" - registration_enabled? false - code_verifier true - client_secret fn _, _ -> - Application.fetch_env(:mv, :oicd_client_secret) + client_id "mv" + base_url "http://localhost:8080/auth/v1" + redirect_uri "http://localhost:4000/auth/user/rauthy/callback" + auth_method :client_secret_jwt + code_verifier true + + client_secret fn _, _ -> + Application.fetch_env(:mv, :oicd_client_secret) end end @@ -62,9 +68,24 @@ defmodule Mv.Accounts.User do filter expr(email == get_path(^arg(:user_info), [:email])) end - end - ## TODO: registration ergänzen, seed rausnehmen, oidc_id aus user_info map holen + create :register_with_rauthy do + argument :user_info, :map, allow_nil?: false + argument :oauth_tokens, :map, allow_nil?: false + upsert? true + upsert_identity :unique_email + + change AshAuthentication.GenerateTokenChange + + change fn changeset, _ctx -> + user_info = Ash.Changeset.get_argument(changeset, :user_info) + + changeset + |> Ash.Changeset.change_attribute(:email, user_info["preferred_username"]) + |> Ash.Changeset.change_attribute(:oidc_id, user_info["id"]) + end + end + end attributes do uuid_primary_key :id diff --git a/lib/accounts/user_identity.exs b/lib/accounts/user_identity.exs index 1fe54f8..fd8d2c9 100644 --- a/lib/accounts/user_identity.exs +++ b/lib/accounts/user_identity.exs @@ -1,15 +1,18 @@ defmodule Mv.Accounts.UserIdentity do + @moduledoc """ + AshAuthentication specific ressource + """ use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication.UserIdentity], domain: Mv.Accounts - user_identity do - user_resource Mv.Accounts.User - end - postgres do table "user_identities" repo Mv.Repo end + + user_identity do + user_resource Mv.Accounts.User + end end diff --git a/lib/mv_web/controllers/auth_controller.ex b/lib/mv_web/controllers/auth_controller.ex index f3dd287..613c8d1 100644 --- a/lib/mv_web/controllers/auth_controller.ex +++ b/lib/mv_web/controllers/auth_controller.ex @@ -22,8 +22,6 @@ defmodule MvWeb.AuthController do end def failure(conn, activity, reason) do - IO.puts(inspect(reason)) - message = case {activity, reason} do {_, @@ -50,7 +48,7 @@ defmodule MvWeb.AuthController do return_to = get_session(conn, :return_to) || ~p"/" conn - |> clear_session() + |> clear_session(:mv) |> put_flash(:info, "You are now signed out") |> redirect(to: return_to) end diff --git a/lib/mv_web/controllers/page_html/home.html.heex b/lib/mv_web/controllers/page_html/home.html.heex index 8cf0506..f13765e 100644 --- a/lib/mv_web/controllers/page_html/home.html.heex +++ b/lib/mv_web/controllers/page_html/home.html.heex @@ -1,3 +1,6 @@ +