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 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/.gitignore b/.gitignore index 040944d..eef8464 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,6 @@ npm-debug.log /assets/node_modules/ .cursor + +# Ignore the .env file with env variables +.env 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/Justfile b/Justfile index 19a93bf..26db3bc 100644 --- a/Justfile +++ b/Justfile @@ -1,3 +1,5 @@ +set dotenv-load := true + run: install-dependencies start-database migrate-database seed-database mix phx.server 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/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/config/dev.exs b/config/dev.exs index b7f9ad7..17b4ce1 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -84,3 +84,19 @@ 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" + +config :mv, :rauthy, + client_id: "mv", + base_url: "http://localhost:8080/auth/v1", + client_secret: System.get_env("OIDC_CLIENT_SECRET"), + redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + +# AshAuthentication development configuration +config :mv, :session_identifier, :jti + +config :mv, :require_token_presence_for_authentication, true diff --git a/config/runtime.exs b/config/runtime.exs index e591590..e8ab249 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -53,6 +53,13 @@ if config_env() == :prod do config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") + config :mv, :rauthy, redirect_uri: "http://localhost:4000/auth/user/rauthy/callback" + + # AshAuthentication production configuration + config :mv, :session_identifier, :jti + + config :mv, :require_token_presence_for_authentication, true + config :mv, MvWeb.Endpoint, url: [host: host, port: 443, scheme: "https"], http: [ diff --git a/config/test.exs b/config/test.exs index 01a8ae8..bcb55eb 100644 --- a/config/test.exs +++ b/config/test.exs @@ -36,3 +36,12 @@ config :phoenix, :plug_init_mode, :runtime # Enable helpful, but potentially expensive runtime checks config :phoenix_live_view, enable_expensive_runtime_checks: true + +# Token signing secret for AshAuthentication tests +config :mv, :token_signing_secret, "test_secret_key_for_ash_authentication_tokens" + +# AshAuthentication test-specific configuration +# In Tests we don't need token presence, but in other envs its recommended +config :mv, :session_identifier, :unsafe + +config :mv, :require_token_presence_for_authentication, false diff --git a/docker-compose.yml b/docker-compose.yml index 3b4e8ec..fc911c3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,10 @@ +version: "3.5" + +networks: + local: + rauthy-dev: + driver: bridge + services: db: image: postgres:17.5-alpine @@ -16,9 +23,46 @@ services: networks: - local -networks: - local: + mailcrab: + image: marlonb/mailcrab:latest + ports: + - "1080:1080" + networks: + - rauthy-dev + + + rauthy: + container_name: rauthy-dev + image: ghcr.io/sebadob/rauthy:0.30.2 + environment: + - LOCAL_TEST=true + - SMTP_URL=mailcrab + - SMTP_PORT=1025 + - SMTP_DANGER_INSECURE=true + - LISTEN_SCHEME=http + - PUB_URL=localhost:8080 + - BOOTSTRAP_ADMIN_PASSWORD_PLAIN=RauthyTest12345 + #- 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-dev + - local + volumes: + - type: volume + source: rauthy-data + target: /app/data volumes: postgres-data: + rauthy-data: + diff --git a/lib/accounts/accounts.ex b/lib/accounts/accounts.ex new file mode 100644 index 0000000..55e8a4b --- /dev/null +++ b/lib/accounts/accounts.ex @@ -0,0 +1,18 @@ +defmodule Mv.Accounts do + @moduledoc """ + AshAuthentication specific domain to handle Authentication for users. + """ + 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 + + resource Mv.Accounts.Token + end +end diff --git a/lib/accounts/token.ex b/lib/accounts/token.ex new file mode 100644 index 0000000..ab9c3a7 --- /dev/null +++ b/lib/accounts/token.ex @@ -0,0 +1,14 @@ +defmodule Mv.Accounts.Token do + @moduledoc """ + AshAuthentication specific ressource + """ + 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 new file mode 100644 index 0000000..15e3a22 --- /dev/null +++ b/lib/accounts/user.ex @@ -0,0 +1,126 @@ +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, + extensions: [AshAuthentication] + + # authorizers: [Ash.Policy.Authorizer] + + postgres do + table "users" + 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 + session_identifier Application.get_env(:mv, :session_identifier) + + tokens do + enabled? true + token_resource Mv.Accounts.Token + + require_token_presence_for_authentication? Application.get_env( + :mv, + :require_token_presence_for_authentication + ) + + store_all_tokens? true + + # signing_algorithm "EdDSA" -> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87 + + signing_secret fn _, _ -> + {:ok, Application.get_env(:mv, :token_signing_secret)} + end + end + + strategies do + oidc :rauthy do + client_id Mv.Secrets + base_url Mv.Secrets + redirect_uri Mv.Secrets + client_secret Mv.Secrets + auth_method :client_secret_jwt + code_verifier true + + # id_token_signed_response_alg "EdDSA" #-> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87 + end + + password :password do + identity_field :email + hash_provider AshAuthentication.BcryptProvider + confirmation_required? false + end + end + end + + actions do + 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_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 + + 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 + + attribute :email, :ci_string, allow_nil?: false, public?: true + attribute :hashed_password, :string, sensitive?: true, allow_nil?: true + attribute :oidc_id, :string, allow_nil?: true + end + + relationships do + belongs_to :member, Mv.Membership.Member + end + + 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 + # 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/accounts/user_identity.exs b/lib/accounts/user_identity.exs new file mode 100644 index 0000000..fd8d2c9 --- /dev/null +++ b/lib/accounts/user_identity.exs @@ -0,0 +1,18 @@ +defmodule Mv.Accounts.UserIdentity do + @moduledoc """ + AshAuthentication specific ressource + """ + use Ash.Resource, + data_layer: AshPostgres.DataLayer, + extensions: [AshAuthentication.UserIdentity], + domain: Mv.Accounts + + postgres do + table "user_identities" + repo Mv.Repo + end + + user_identity do + user_resource Mv.Accounts.User + end +end 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) 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/secrets.ex b/lib/mv/secrets.ex new file mode 100644 index 0000000..6a88eee --- /dev/null +++ b/lib/mv/secrets.ex @@ -0,0 +1,46 @@ +defmodule Mv.Secrets do + use AshAuthentication.Secret + + def secret_for( + [:authentication, :strategies, :rauthy, :client_id], + Mv.Accounts.User, + _opts, + _meth + ) do + get_config(:client_id) + end + + def secret_for( + [:authentication, :strategies, :rauthy, :redirect_uri], + Mv.Accounts.User, + _opts, + _meth + ) do + get_config(:redirect_uri) + end + + def secret_for( + [:authentication, :strategies, :rauthy, :client_secret], + Mv.Accounts.User, + _opts, + _meth + ) do + get_config(:client_secret) + end + + def secret_for( + [:authentication, :strategies, :rauthy, :base_url], + Mv.Accounts.User, + _opts, + _meth + ) do + get_config(:base_url) + end + + defp get_config(key) do + :mv + |> Application.fetch_env!(:rauthy) + |> Keyword.fetch!(key) + |> then(&{:ok, &1}) + end +end 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..42299f4 --- /dev/null +++ b/lib/mv_web/controllers/auth_controller.ex @@ -0,0 +1,59 @@ +require Logger + +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} -> gettext("Your email address has now been confirmed") + {:password, :reset} -> gettext("Your password has successfully been reset") + _ -> gettext("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 + Logger.error(%{conn: conn, reason: reason}) + + message = + case {activity, reason} do + {_, + %AshAuthentication.Errors.AuthenticationFailed{ + caused_by: %Ash.Error.Forbidden{ + errors: [%AshAuthentication.Errors.CannotConfirmUnconfirmedUser{}] + } + }} -> gettext( + """ + 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. + """) + + _ -> + gettext("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(:mv) + |> put_flash(:info, gettext("You are now signed out")) + |> redirect(to: return_to) + end +end diff --git a/lib/mv_web/controllers/page_html/home.html.heex b/lib/mv_web/controllers/page_html/home.html.heex index d72b03c..f13765e 100644 --- a/lib/mv_web/controllers/page_html/home.html.heex +++ b/lib/mv_web/controllers/page_html/home.html.heex @@ -1,222 +1,55 @@ -<.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/live_user_auth.ex b/lib/mv_web/live_user_auth.ex new file mode 100644 index 0000000..67bef70 --- /dev/null +++ b/lib/mv_web/live_user_auth.ex @@ -0,0 +1,46 @@ +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/router.ex b/lib/mv_web/router.ex index f2cde75..595a2da 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,36 +12,92 @@ 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 - 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 :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 - 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 + scope "/", MvWeb do + pipe_through :browser - 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 + @doc """ + AshAuthentication-specific: We define that all routes can only be accessed when the user is signed in. + """ + ash_authentication_live_session :authentication_required, + on_mount: {MvWeb.LiveUserAuth, :live_user_required} do + get "/", PageController, :home - post "/set_locale", LocaleController, :set_locale + 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 + + 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 + + post "/set_locale", LocaleController, :set_locale + end + + # ASHAUTHENTICATION GENERATED AUTH ROUTES + 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], + gettext_backend: {MvWeb.Gettext, "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], + gettext_backend: {MvWeb.Gettext, "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], + gettext_backend: {MvWeb.Gettext, "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..c16e11b 100644 --- a/mix.exs +++ b/mix.exs @@ -40,6 +40,9 @@ defmodule Mv.MixProject do {:ash_postgres, "~> 2.0"}, {:ash_phoenix, "~> 2.0"}, {:ash, "~> 3.0"}, + {:bcrypt_elixir, "~> 3.0"}, + {:ash_authentication, "~> 4.9"}, + {:ash_authentication_phoenix, "~> 2.10"}, {:igniter, "~> 0.6", only: [:dev, :test]}, {:phoenix, "~> 1.7.20"}, {:phoenix_ecto, "~> 4.5"}, @@ -92,7 +95,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..51b4604 100644 --- a/mix.lock +++ b/mix.lock @@ -1,13 +1,18 @@ %{ "ash": {:hex, :ash, "3.5.19", "defd1c6b94475352a7b69f430b792fb64e3a9f7ca030195737bb97dc0f1311b5", [: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.6.4 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.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.65 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", "ded976230b1ef823aeb25008cc62de6545bf3ad6208cf1f3badb598fa6c01375"}, "ash_admin": {:hex, :ash_admin, "0.13.9", "8a7c0f52be4aa490e4a59137bc40e3abafba9e1977f800bb2edae3f331ef1ebb", [: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", "1373e1749d6b5b21c7ff7d7fc79ac932f6f8d1bd0d154a80758eab168948ea37"}, + "ash_authentication": {:hex, :ash_authentication, "4.9.3", "2347b7982e3b00ae1165a4ef6875e05540204e933922e302bd3ac2be4c043e20", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {: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", "1641988f6c67b7d7517caed9e6cb0f6bd906bbb994e2831022b6ad7cecf45ad0"}, + "ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.10.1", "6facb8e14d7e93c3268b8cb5300d42d3802bd754d241f4215f2c5fc1d34c4c94", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, ">= 4.9.1 and < 5.0.0-0", [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", "efc27905b29476cacb67562658d5b38ca0656b3c81c4bcb40a117a2d8d686433"}, "ash_phoenix": {:hex, :ash_phoenix, "2.3.6", "c2bea1673af52f305b2fe0c04999bd1f0dc8e127d4757a3d7f42d0b9dea16a7a", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, 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", "6923dca70fe1d533864134999f4d9c5c59ef745a6b50982d42d60c18966474cd"}, "ash_postgres": {:hex, :ash_postgres, "2.6.6", "f60f806e3e969669329dfd33068bf602f3d7f214e0bbb36c241433f34cbff2e0", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.72 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.6", [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", "3133800432273f9e6effb6f8464fe81da22c5b577aa73291f63fd229f4bb43fb"}, "ash_sql": {:hex, :ash_sql, "0.2.80", "7717dca3794d7461b8302b107f039bce2c57773840177528cf94c7c264ed763b", [: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", "036f96b78bf612a1d1fe798b8795ab1e6ecef81e41ca473b1533b139dd0202ab"}, + "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.7.0", "d1564f30553c97d3e25f9623144bb8df11f3787a26733f00b21699a128105c0c", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [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", "3e2f7a98c7a11f48d9d8c037f7177cd39778e74d55c7af06fe6227c742a8168a"}, + "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.14", "4582dd7d630b48cf5e1ca8d3d42494db51e406b7ba704e81fbd401866366896a", [:mix], [], "hexpm", "7bc1b65249d31701393edaaac18ec8398d8974d52c647b7904d01b964137b9f4"}, "circular_buffer": {:hex, :circular_buffer, "0.4.1", "477f370fd8cfe1787b0a1bade6208bbd274b34f1610e41f1180ba756a7679839", [:mix], [], "hexpm", "633ef2e059dde0d7b89bbab13b1da9d04c6685e80e68fbdf41282d4fae746b72"}, + "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"}, @@ -15,6 +20,7 @@ "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"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, "esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"}, "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"}, @@ -30,6 +36,8 @@ "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"}, @@ -42,6 +50,7 @@ "phoenix": {:hex, :phoenix, "1.7.21", "14ca4f1071a5f65121217d6b57ac5712d1857e40a0833aff7a691b7870fc9a3b", [: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", "336dce4f86cba56fed312a7d280bf2282c720abb6074bdb1b61ec8095bdd0bc9"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.4", "dcf3483ab45bab4c15e3a47c34451392f64e433846b08469f5d16c2a4cd70052", [: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", "f5b8584c36ccc9b903948a696fc9b8b81102c79c7c0c751a9f00cdec55d5f2d7"}, "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.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [: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", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.6.0", "2791fac0e2776b640192308cc90c0dbcf67843ad51387ed4ecae2038263d708d", [:mix], [{:file_system, "~> 0.2.10 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b3a1fa036d7eb2f956774eda7a7638cf5123f8f2175aca6d6420a7f95e598e1c"}, "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.17", "beeb16d83a7d3760f7ad463df94e83b087577665d2acc0bf2987cd7d9778068f", [: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 or ~> 1.8.0-rc", [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", "a4ca05c1eb6922c4d07a508a75bfa12c45e5f4d8f77ae83283465f02c53741e1"}, @@ -54,6 +63,7 @@ "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"}, + "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, "sobelow": {:hex, :sobelow, "0.14.0", "dd82aae8f72503f924fe9dd97ffe4ca694d2f17ec463dcfd365987c9752af6ee", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7ecf91e298acfd9b24f5d761f19e8f6e6ac585b9387fb6301023f1f2cd5eed5f"}, "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"}, diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po index aa33cc3..7e6d755 100644 --- a/priv/gettext/de/LC_MESSAGES/default.po +++ b/priv/gettext/de/LC_MESSAGES/default.po @@ -25,9 +25,9 @@ msgstr "Bist du sicher?" msgid "Attempting to reconnect" msgstr "Verbindung wird wiederhergestellt" -#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/form_component.ex:50 #: lib/mv_web/member_live/index.ex:25 -#: lib/mv_web/member_live/show.ex:30 +#: lib/mv_web/member_live/show.ex:32 #, elixir-autogen, elixir-format msgid "City" msgstr "Stadt" @@ -43,12 +43,12 @@ msgid "Edit" msgstr "Bearbeiten" #: lib/mv_web/member_live/index.ex:76 -#: lib/mv_web/member_live/show.ex:91 +#: lib/mv_web/member_live/show.ex:93 #, elixir-autogen, elixir-format msgid "Edit Member" msgstr "Mitglied bearbeiten" -#: lib/mv_web/member_live/form_component.ex:41 +#: lib/mv_web/member_live/form_component.ex:43 #: lib/mv_web/member_live/index.ex:24 #: lib/mv_web/member_live/show.ex:23 #, elixir-autogen, elixir-format @@ -60,7 +60,7 @@ msgstr "E-Mail" msgid "Error!" msgstr "Fehler!" -#: lib/mv_web/member_live/form_component.ex:39 +#: lib/mv_web/member_live/form_component.ex:41 #: lib/mv_web/member_live/index.ex:22 #: lib/mv_web/member_live/show.ex:21 #, elixir-autogen, elixir-format @@ -72,14 +72,14 @@ msgstr "Vorname" msgid "Hang in there while we get back on track" msgstr "Bitte warten, wir stellen die Verbindung wieder her." -#: lib/mv_web/member_live/form_component.ex:45 +#: lib/mv_web/member_live/form_component.ex:47 #: lib/mv_web/member_live/index.ex:26 -#: lib/mv_web/member_live/show.ex:27 +#: lib/mv_web/member_live/show.ex:29 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "Beitrittsdatum" -#: lib/mv_web/member_live/form_component.ex:40 +#: lib/mv_web/member_live/form_component.ex:42 #: lib/mv_web/member_live/index.ex:23 #: lib/mv_web/member_live/show.ex:22 #, elixir-autogen, elixir-format @@ -124,76 +124,76 @@ msgstr "Keine Internetverbindung gefunden" msgid "close" msgstr "schließen" -#: lib/mv_web/member_live/form_component.ex:42 +#: lib/mv_web/member_live/form_component.ex:44 #: lib/mv_web/member_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Birth Date" msgstr "Geburtsdatum" -#: lib/mv_web/member_live/form_component.ex:53 -#: lib/mv_web/member_live/show.ex:36 +#: lib/mv_web/member_live/form_component.ex:55 +#: lib/mv_web/member_live/show.ex:38 #, elixir-autogen, elixir-format msgid "Custom Properties" msgstr "Eigene Eigenschaften" -#: lib/mv_web/member_live/form_component.ex:46 -#: lib/mv_web/member_live/show.ex:28 +#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/show.ex:30 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "Austrittsdatum" -#: lib/mv_web/member_live/form_component.ex:50 -#: lib/mv_web/member_live/show.ex:32 +#: lib/mv_web/member_live/form_component.ex:52 +#: lib/mv_web/member_live/show.ex:34 #, elixir-autogen, elixir-format msgid "House Number" msgstr "Hausnummer" -#: lib/mv_web/member_live/form_component.ex:47 -#: lib/mv_web/member_live/show.ex:29 +#: lib/mv_web/member_live/form_component.ex:49 +#: lib/mv_web/member_live/show.ex:31 #, elixir-autogen, elixir-format msgid "Notes" msgstr "Notizen" -#: lib/mv_web/member_live/form_component.ex:43 +#: lib/mv_web/member_live/form_component.ex:45 #: lib/mv_web/member_live/show.ex:25 #, elixir-autogen, elixir-format msgid "Paid" msgstr "Bezahlt" -#: lib/mv_web/member_live/form_component.ex:44 -#: lib/mv_web/member_live/show.ex:26 +#: lib/mv_web/member_live/form_component.ex:46 +#: lib/mv_web/member_live/show.ex:28 #, elixir-autogen, elixir-format msgid "Phone Number" msgstr "Telefonnummer" -#: lib/mv_web/member_live/form_component.ex:51 -#: lib/mv_web/member_live/show.ex:33 +#: lib/mv_web/member_live/form_component.ex:53 +#: lib/mv_web/member_live/show.ex:35 #, elixir-autogen, elixir-format msgid "Postal Code" msgstr "Postleitzahl" -#: lib/mv_web/member_live/form_component.ex:73 +#: lib/mv_web/member_live/form_component.ex:75 #, elixir-autogen, elixir-format msgid "Save Member" msgstr "Mitglied speichern" -#: lib/mv_web/member_live/form_component.ex:73 +#: lib/mv_web/member_live/form_component.ex:75 #, elixir-autogen, elixir-format msgid "Saving..." msgstr "Speichern..." -#: lib/mv_web/member_live/form_component.ex:49 -#: lib/mv_web/member_live/show.ex:31 +#: lib/mv_web/member_live/form_component.ex:51 +#: lib/mv_web/member_live/show.ex:33 #, elixir-autogen, elixir-format msgid "Street" msgstr "Straße" -#: lib/mv_web/member_live/form_component.ex:29 +#: lib/mv_web/member_live/form_component.ex:30 #, elixir-autogen, elixir-format msgid "Use this form to manage member records and their properties." msgstr "Dieses Formular dient zur Verwaltung von Mitgliedern und deren Eigenschaften." -#: lib/mv_web/member_live/show.ex:50 +#: lib/mv_web/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Back to members" msgstr "Zurück zur Mitgliederliste" @@ -208,12 +208,12 @@ msgstr "Mitglied bearbeiten" msgid "Id" msgstr "ID" -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "No" msgstr "Nein" -#: lib/mv_web/member_live/show.ex:90 +#: lib/mv_web/member_live/show.ex:92 #, elixir-autogen, elixir-format, fuzzy msgid "Show Member" msgstr "Mitglied anzeigen" @@ -223,22 +223,52 @@ msgstr "Mitglied anzeigen" msgid "This is a member record from your database." msgstr "Dies ist ein Mitglied aus deiner Datenbank." -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "Yes" msgstr "Ja" -#: lib/mv_web/member_live/form_component.ex:107 -#, elixir-autogen, elixir-format -msgid "Member %{action} successfully" -msgstr "Mitglied %{action} erfolgreich" - -#: lib/mv_web/member_live/form_component.ex:100 +#: lib/mv_web/member_live/form_component.ex:102 #, elixir-autogen, elixir-format msgid "create" msgstr "erstellt" -#: lib/mv_web/member_live/form_component.ex:101 +#: lib/mv_web/member_live/form_component.ex:103 #, elixir-autogen, elixir-format msgid "update" msgstr "aktualisiert" + +#: lib/mv_web/controllers/auth_controller.ex:43 +#, elixir-autogen, elixir-format +msgid "Incorrect email or password" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:109 +#, elixir-autogen, elixir-format +msgid "Member %{action} successfully" +msgstr "Mitglied %{action} erfolgreich" + +#: lib/mv_web/controllers/auth_controller.ex:14 +#, elixir-autogen, elixir-format +msgid "You are now signed in" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:56 +#, elixir-autogen, elixir-format +msgid "You are now signed out" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:36 +#, elixir-autogen, elixir-format +msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:12 +#, elixir-autogen, elixir-format +msgid "Your email address has now been confirmed" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:13 +#, elixir-autogen, elixir-format +msgid "Your password has successfully been reset" +msgstr "" diff --git a/priv/gettext/de/LC_MESSAGES/errors.po b/priv/gettext/de/LC_MESSAGES/errors.po index c0fba6d..844c4f5 100644 --- a/priv/gettext/de/LC_MESSAGES/errors.po +++ b/priv/gettext/de/LC_MESSAGES/errors.po @@ -110,24 +110,3 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" - -msgid "is not a valid email" -msgstr "ist keine gültige E-Mail-Adresse" - -msgid "cannot be in the future" -msgstr "darf nicht in der Zukunft liegen" - -msgid "must be present" -msgstr "muss ausgefüllt sein" - -msgid "is not a valid phone number" -msgstr "ist keine gültige Telefonnummer" - -msgid "length must be greater than or equal to 5" -msgstr "Die Länge muss mindestens 5 Zeichen betragen" - -msgid "cannot be before join date" -msgstr "darf nicht vor dem Eintrittsdatum liegen" - -msgid "must consist of 5 digits" -msgstr "muss aus 5 Ziffern bestehen" diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index f5b79d7..aad2ae9 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -26,9 +26,9 @@ msgstr "" msgid "Attempting to reconnect" msgstr "" -#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/form_component.ex:50 #: lib/mv_web/member_live/index.ex:25 -#: lib/mv_web/member_live/show.ex:30 +#: lib/mv_web/member_live/show.ex:32 #, elixir-autogen, elixir-format msgid "City" msgstr "" @@ -44,12 +44,12 @@ msgid "Edit" msgstr "" #: lib/mv_web/member_live/index.ex:76 -#: lib/mv_web/member_live/show.ex:91 +#: lib/mv_web/member_live/show.ex:93 #, elixir-autogen, elixir-format msgid "Edit Member" msgstr "" -#: lib/mv_web/member_live/form_component.ex:41 +#: lib/mv_web/member_live/form_component.ex:43 #: lib/mv_web/member_live/index.ex:24 #: lib/mv_web/member_live/show.ex:23 #, elixir-autogen, elixir-format @@ -61,7 +61,7 @@ msgstr "" msgid "Error!" msgstr "" -#: lib/mv_web/member_live/form_component.ex:39 +#: lib/mv_web/member_live/form_component.ex:41 #: lib/mv_web/member_live/index.ex:22 #: lib/mv_web/member_live/show.ex:21 #, elixir-autogen, elixir-format @@ -73,14 +73,14 @@ msgstr "" msgid "Hang in there while we get back on track" msgstr "" -#: lib/mv_web/member_live/form_component.ex:45 +#: lib/mv_web/member_live/form_component.ex:47 #: lib/mv_web/member_live/index.ex:26 -#: lib/mv_web/member_live/show.ex:27 +#: lib/mv_web/member_live/show.ex:29 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:40 +#: lib/mv_web/member_live/form_component.ex:42 #: lib/mv_web/member_live/index.ex:23 #: lib/mv_web/member_live/show.ex:22 #, elixir-autogen, elixir-format @@ -125,76 +125,76 @@ msgstr "" msgid "close" msgstr "" -#: lib/mv_web/member_live/form_component.ex:42 +#: lib/mv_web/member_live/form_component.ex:44 #: lib/mv_web/member_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Birth Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:53 -#: lib/mv_web/member_live/show.ex:36 +#: lib/mv_web/member_live/form_component.ex:55 +#: lib/mv_web/member_live/show.ex:38 #, elixir-autogen, elixir-format msgid "Custom Properties" msgstr "" -#: lib/mv_web/member_live/form_component.ex:46 -#: lib/mv_web/member_live/show.ex:28 +#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/show.ex:30 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:50 -#: lib/mv_web/member_live/show.ex:32 +#: lib/mv_web/member_live/form_component.ex:52 +#: lib/mv_web/member_live/show.ex:34 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/member_live/form_component.ex:47 -#: lib/mv_web/member_live/show.ex:29 -#, elixir-autogen, elixir-format -msgid "Notes" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:43 -#: lib/mv_web/member_live/show.ex:25 -#, elixir-autogen, elixir-format -msgid "Paid" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:44 -#: lib/mv_web/member_live/show.ex:26 -#, elixir-autogen, elixir-format -msgid "Phone Number" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:51 -#: lib/mv_web/member_live/show.ex:33 -#, elixir-autogen, elixir-format -msgid "Postal Code" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:73 -#, elixir-autogen, elixir-format -msgid "Save Member" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:73 -#, elixir-autogen, elixir-format -msgid "Saving..." -msgstr "" - #: lib/mv_web/member_live/form_component.ex:49 #: lib/mv_web/member_live/show.ex:31 #, elixir-autogen, elixir-format +msgid "Notes" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:45 +#: lib/mv_web/member_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "Paid" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:46 +#: lib/mv_web/member_live/show.ex:28 +#, elixir-autogen, elixir-format +msgid "Phone Number" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:53 +#: lib/mv_web/member_live/show.ex:35 +#, elixir-autogen, elixir-format +msgid "Postal Code" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:75 +#, elixir-autogen, elixir-format +msgid "Save Member" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:75 +#, elixir-autogen, elixir-format +msgid "Saving..." +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:51 +#: lib/mv_web/member_live/show.ex:33 +#, elixir-autogen, elixir-format msgid "Street" msgstr "" -#: lib/mv_web/member_live/form_component.ex:29 +#: lib/mv_web/member_live/form_component.ex:30 #, elixir-autogen, elixir-format msgid "Use this form to manage member records and their properties." msgstr "" -#: lib/mv_web/member_live/show.ex:50 +#: lib/mv_web/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Back to members" msgstr "" @@ -209,12 +209,12 @@ msgstr "" msgid "Id" msgstr "" -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "No" msgstr "" -#: lib/mv_web/member_live/show.ex:90 +#: lib/mv_web/member_live/show.ex:92 #, elixir-autogen, elixir-format msgid "Show Member" msgstr "" @@ -224,22 +224,52 @@ msgstr "" msgid "This is a member record from your database." msgstr "" -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" -#: lib/mv_web/member_live/form_component.ex:107 -#, elixir-autogen, elixir-format -msgid "Mitglied %{action} erfolgreich" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:100 +#: lib/mv_web/member_live/form_component.ex:102 #, elixir-autogen, elixir-format msgid "create" msgstr "" -#: lib/mv_web/member_live/form_component.ex:101 +#: lib/mv_web/member_live/form_component.ex:103 #, elixir-autogen, elixir-format msgid "update" msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:43 +#, elixir-autogen, elixir-format +msgid "Incorrect email or password" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:109 +#, elixir-autogen, elixir-format +msgid "Member %{action} successfully" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:14 +#, elixir-autogen, elixir-format +msgid "You are now signed in" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:56 +#, elixir-autogen, elixir-format +msgid "You are now signed out" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:36 +#, elixir-autogen, elixir-format +msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:12 +#, elixir-autogen, elixir-format +msgid "Your email address has now been confirmed" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:13 +#, elixir-autogen, elixir-format +msgid "Your password has successfully been reset" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index 6173d39..3317236 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -26,9 +26,9 @@ msgstr "" msgid "Attempting to reconnect" msgstr "" -#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/form_component.ex:50 #: lib/mv_web/member_live/index.ex:25 -#: lib/mv_web/member_live/show.ex:30 +#: lib/mv_web/member_live/show.ex:32 #, elixir-autogen, elixir-format msgid "City" msgstr "" @@ -44,12 +44,12 @@ msgid "Edit" msgstr "" #: lib/mv_web/member_live/index.ex:76 -#: lib/mv_web/member_live/show.ex:91 +#: lib/mv_web/member_live/show.ex:93 #, elixir-autogen, elixir-format msgid "Edit Member" msgstr "" -#: lib/mv_web/member_live/form_component.ex:41 +#: lib/mv_web/member_live/form_component.ex:43 #: lib/mv_web/member_live/index.ex:24 #: lib/mv_web/member_live/show.ex:23 #, elixir-autogen, elixir-format @@ -61,7 +61,7 @@ msgstr "" msgid "Error!" msgstr "" -#: lib/mv_web/member_live/form_component.ex:39 +#: lib/mv_web/member_live/form_component.ex:41 #: lib/mv_web/member_live/index.ex:22 #: lib/mv_web/member_live/show.ex:21 #, elixir-autogen, elixir-format @@ -73,14 +73,14 @@ msgstr "" msgid "Hang in there while we get back on track" msgstr "" -#: lib/mv_web/member_live/form_component.ex:45 +#: lib/mv_web/member_live/form_component.ex:47 #: lib/mv_web/member_live/index.ex:26 -#: lib/mv_web/member_live/show.ex:27 +#: lib/mv_web/member_live/show.ex:29 #, elixir-autogen, elixir-format msgid "Join Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:40 +#: lib/mv_web/member_live/form_component.ex:42 #: lib/mv_web/member_live/index.ex:23 #: lib/mv_web/member_live/show.ex:22 #, elixir-autogen, elixir-format @@ -125,76 +125,76 @@ msgstr "" msgid "close" msgstr "" -#: lib/mv_web/member_live/form_component.ex:42 +#: lib/mv_web/member_live/form_component.ex:44 #: lib/mv_web/member_live/show.ex:24 #, elixir-autogen, elixir-format msgid "Birth Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:53 -#: lib/mv_web/member_live/show.ex:36 +#: lib/mv_web/member_live/form_component.ex:55 +#: lib/mv_web/member_live/show.ex:38 #, elixir-autogen, elixir-format msgid "Custom Properties" msgstr "" -#: lib/mv_web/member_live/form_component.ex:46 -#: lib/mv_web/member_live/show.ex:28 +#: lib/mv_web/member_live/form_component.ex:48 +#: lib/mv_web/member_live/show.ex:30 #, elixir-autogen, elixir-format msgid "Exit Date" msgstr "" -#: lib/mv_web/member_live/form_component.ex:50 -#: lib/mv_web/member_live/show.ex:32 +#: lib/mv_web/member_live/form_component.ex:52 +#: lib/mv_web/member_live/show.ex:34 #, elixir-autogen, elixir-format msgid "House Number" msgstr "" -#: lib/mv_web/member_live/form_component.ex:47 -#: lib/mv_web/member_live/show.ex:29 -#, elixir-autogen, elixir-format -msgid "Notes" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:43 -#: lib/mv_web/member_live/show.ex:25 -#, elixir-autogen, elixir-format -msgid "Paid" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:44 -#: lib/mv_web/member_live/show.ex:26 -#, elixir-autogen, elixir-format -msgid "Phone Number" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:51 -#: lib/mv_web/member_live/show.ex:33 -#, elixir-autogen, elixir-format -msgid "Postal Code" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:73 -#, elixir-autogen, elixir-format, fuzzy -msgid "Save Member" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:73 -#, elixir-autogen, elixir-format -msgid "Saving..." -msgstr "" - #: lib/mv_web/member_live/form_component.ex:49 #: lib/mv_web/member_live/show.ex:31 #, elixir-autogen, elixir-format +msgid "Notes" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:45 +#: lib/mv_web/member_live/show.ex:25 +#, elixir-autogen, elixir-format +msgid "Paid" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:46 +#: lib/mv_web/member_live/show.ex:28 +#, elixir-autogen, elixir-format +msgid "Phone Number" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:53 +#: lib/mv_web/member_live/show.ex:35 +#, elixir-autogen, elixir-format +msgid "Postal Code" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:75 +#, elixir-autogen, elixir-format, fuzzy +msgid "Save Member" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:75 +#, elixir-autogen, elixir-format +msgid "Saving..." +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:51 +#: lib/mv_web/member_live/show.ex:33 +#, elixir-autogen, elixir-format msgid "Street" msgstr "" -#: lib/mv_web/member_live/form_component.ex:29 +#: lib/mv_web/member_live/form_component.ex:30 #, elixir-autogen, elixir-format msgid "Use this form to manage member records and their properties." msgstr "" -#: lib/mv_web/member_live/show.ex:50 +#: lib/mv_web/member_live/show.ex:52 #, elixir-autogen, elixir-format msgid "Back to members" msgstr "" @@ -209,12 +209,12 @@ msgstr "" msgid "Id" msgstr "" -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "No" msgstr "" -#: lib/mv_web/member_live/show.ex:90 +#: lib/mv_web/member_live/show.ex:92 #, elixir-autogen, elixir-format, fuzzy msgid "Show Member" msgstr "" @@ -224,22 +224,52 @@ msgstr "" msgid "This is a member record from your database." msgstr "" -#: lib/mv_web/member_live/show.ex:25 +#: lib/mv_web/member_live/show.ex:26 #, elixir-autogen, elixir-format msgid "Yes" msgstr "" -#: lib/mv_web/member_live/form_component.ex:107 -#, elixir-autogen, elixir-format -msgid "Member %{action} successfully" -msgstr "" - -#: lib/mv_web/member_live/form_component.ex:100 +#: lib/mv_web/member_live/form_component.ex:102 #, elixir-autogen, elixir-format msgid "create" msgstr "" -#: lib/mv_web/member_live/form_component.ex:101 +#: lib/mv_web/member_live/form_component.ex:103 #, elixir-autogen, elixir-format msgid "update" msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:43 +#, elixir-autogen, elixir-format +msgid "Incorrect email or password" +msgstr "" + +#: lib/mv_web/member_live/form_component.ex:109 +#, elixir-autogen, elixir-format +msgid "Member %{action} successfully" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:14 +#, elixir-autogen, elixir-format +msgid "You are now signed in" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:56 +#, elixir-autogen, elixir-format +msgid "You are now signed out" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:36 +#, elixir-autogen, elixir-format +msgid "You have already signed in another way, but have not confirmed your account.\nYou can confirm your account using the link we sent to you, or by resetting your password.\n" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:12 +#, elixir-autogen, elixir-format +msgid "Your email address has now been confirmed" +msgstr "" + +#: lib/mv_web/controllers/auth_controller.ex:13 +#, elixir-autogen, elixir-format +msgid "Your password has successfully been reset" +msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/errors.po b/priv/gettext/en/LC_MESSAGES/errors.po index 60c1037..844c4f5 100644 --- a/priv/gettext/en/LC_MESSAGES/errors.po +++ b/priv/gettext/en/LC_MESSAGES/errors.po @@ -110,12 +110,3 @@ msgstr "" msgid "must be equal to %{number}" msgstr "" - -msgid "length must be greater than or equal to 5" -msgstr "length must be greater than or equal to 5" - -msgid "cannot be before join date" -msgstr "cannot be before join date" - -msgid "must consist of 5 digits" -msgstr "must consist of 5 digits" diff --git a/priv/repo/migrations/20250620110849_add_accounts_domain_extensions.exs b/priv/repo/migrations/20250620110849_add_accounts_domain_extensions.exs new file mode 100644 index 0000000..f77419c --- /dev/null +++ b/priv/repo/migrations/20250620110849_add_accounts_domain_extensions.exs @@ -0,0 +1,19 @@ +defmodule Mv.Repo.Migrations.AddAccountsDomainExtensions 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/20250620110850_add_accounts_domain.exs b/priv/repo/migrations/20250620110850_add_accounts_domain.exs new file mode 100644 index 0000000..4c7c54b --- /dev/null +++ b/priv/repo/migrations/20250620110850_add_accounts_domain.exs @@ -0,0 +1,58 @@ +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 + create table(:users, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :email, :citext, null: false + add :hashed_password, :text + add :oidc_id, :text + + add :member_id, + references(:members, + column: :id, + name: "users_member_id_fkey", + type: :uuid, + prefix: "public" + ) + end + + create unique_index(:users, [:email], name: "users_unique_email_index") + + create unique_index(:users, [:oidc_id], name: "users_unique_oidc_id_index") + + create table(:tokens, primary_key: false) do + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :created_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :extra_data, :map + add :purpose, :text, null: false + add :expires_at, :utc_datetime, null: false + add :subject, :text, null: false + add :jti, :text, null: false, primary_key: true + end + end + + def down do + drop table(:tokens) + + drop_if_exists unique_index(:users, [:oidc_id], name: "users_unique_oidc_id_index") + + drop_if_exists unique_index(:users, [:email], name: "users_unique_email_index") + + drop constraint(:users, "users_member_id_fkey") + + drop table(:users) + 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/20250620110850.json b/priv/resource_snapshots/repo/tokens/20250620110850.json new file mode 100644 index 0000000..c702eff --- /dev/null +++ b/priv/resource_snapshots/repo/tokens/20250620110850.json @@ -0,0 +1,103 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "created_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "extra_data", + "type": "map" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "purpose", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "expires_at", + "type": "utc_datetime" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "subject", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "jti", + "type": "text" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "EA1475C339B5BE2728560EFB2AF911275B2F65C2CE66CD1C093FAB5D9183BB11", + "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/20250620110850.json b/priv/resource_snapshots/repo/users/20250620110850.json new file mode 100644 index 0000000..54688a8 --- /dev/null +++ b/priv/resource_snapshots/repo/users/20250620110850.json @@ -0,0 +1,127 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "email", + "type": "citext" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "hashed_password", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "oidc_id", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "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" + }, + "scale": null, + "size": null, + "source": "member_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "03EBA1A8BCE47C4706E2D718E00364465E08C9A3999988D49FC1B89DEC5D717C", + "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 + }, + { + "all_tenants?": false, + "base_filter": null, + "index_name": "users_unique_oidc_id_index", + "keys": [ + { + "type": "atom", + "value": "oidc_id" + } + ], + "name": "unique_oidc_id", + "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 diff --git a/test/mv_web/controllers/auth_controller_test.exs b/test/mv_web/controllers/auth_controller_test.exs new file mode 100644 index 0000000..e18ade2 --- /dev/null +++ b/test/mv_web/controllers/auth_controller_test.exs @@ -0,0 +1,48 @@ +defmodule MvWeb.AuthControllerTest do + use MvWeb.ConnCase, async: true + + test "GET /sign-in shows sign in form", %{conn: conn} do + conn = get(conn, ~p"/sign-in") + assert html_response(conn, 200) =~ "Sign in" + end + + test "POST /sign-in with valid credentials redirects to home", %{conn: conn} do + # Create a test user first + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/sign-in") + + assert redirected_to(conn) == ~p"/" + end + + test "POST /sign-in with invalid credentials shows error", %{conn: conn} do + conn = + post(conn, ~p"/auth/sign_in", %{ + "user" => %{ + "email" => "wrong@example.com", + "password" => "wrongpassword" + } + }) + + assert conn.status == 404 + end + + test "GET /sign-out redirects to home", %{conn: conn} do + # First sign in a user + conn = conn_with_oidc_user(conn) + + # Then sign out + conn = get(conn, ~p"/sign-out") + assert redirected_to(conn) == ~p"/" + end + + test "unauthenticated user accessing protected route gets redirected to sign-in", %{conn: conn} do + conn = get(conn, ~p"/members") + assert redirected_to(conn) == ~p"/sign-in" + end + + test "authenticated user can access protected route", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/members") + assert conn.status == 200 + end +end diff --git a/test/mv_web/controllers/page_controller_test.exs b/test/mv_web/controllers/page_controller_test.exs index 702cd78..ce3195b 100644 --- a/test/mv_web/controllers/page_controller_test.exs +++ b/test/mv_web/controllers/page_controller_test.exs @@ -2,7 +2,9 @@ defmodule MvWeb.PageControllerTest do use MvWeb.ConnCase test "GET /", %{conn: conn} do + conn = conn_with_oidc_user(conn) + conn = get(conn, ~p"/") - assert html_response(conn, 200) =~ "Peace of mind from prototype to production" + assert html_response(conn, 200) =~ "Mitgliederverwaltung" end end diff --git a/test/mv_web/member_live/index_test.exs b/test/mv_web/member_live/index_test.exs index b5a5968..ce47a43 100644 --- a/test/mv_web/member_live/index_test.exs +++ b/test/mv_web/member_live/index_test.exs @@ -3,6 +3,7 @@ defmodule MvWeb.MemberLive.IndexTest do import Phoenix.LiveViewTest test "shows translated title in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, _view, html} = live(conn, "/members") # Expected German title @@ -10,6 +11,7 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated title in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) Gettext.put_locale(MvWeb.Gettext, "en") {:ok, _view, html} = live(conn, "/members") # Expected English title @@ -17,18 +19,21 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated button text in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, _view, html} = live(conn, "/members/new") assert html =~ "Speichern" end test "shows translated button text in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) Gettext.put_locale(MvWeb.Gettext, "en") {:ok, _view, html} = live(conn, "/members/new") assert html =~ "Save" end test "shows translated flash message after creating a member in German", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "de") {:ok, view, _html} = live(conn, "/members") view |> element("a", "Neues Mitglied") |> render_click() @@ -44,6 +49,7 @@ defmodule MvWeb.MemberLive.IndexTest do end test "shows translated flash message after creating a member in English", %{conn: conn} do + conn = conn_with_oidc_user(conn) conn = Plug.Test.init_test_session(conn, locale: "en") {:ok, view, _html} = live(conn, "/members") view |> element("a", "New Member") |> render_click() diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 7101531..d1804b7 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -31,6 +31,38 @@ defmodule MvWeb.ConnCase do end end + @doc """ + Creates a test user and returns the user struct. + """ + def create_test_user(attrs \\ %{}) do + email = "user@example.com" + password = "password" + {:ok, hashed_password} = AshAuthentication.BcryptProvider.hash(password) + + Ash.Seed.seed!(Mv.Accounts.User, %{ + email: email, + hashed_password: hashed_password + }) + end + + @doc """ + Signs in a user via OIDC for testing by creating a session with the user's token. + """ + def sign_in_user_via_oidc(conn, user) do + # Mock OIDC sign-in by creating a token directly + conn + |> Phoenix.ConnTest.init_test_session(%{}) + |> AshAuthentication.Plug.Helpers.store_in_session(user) + end + + @doc """ + Signs in a user via OIDC and returns a connection with the user authenticated. + """ + def conn_with_oidc_user(conn, user_attrs \\ %{}) do + user = create_test_user(user_attrs) + sign_in_user_via_oidc(conn, user) + end + setup tags do Mv.DataCase.setup_sandbox(tags) {:ok, conn: Phoenix.ConnTest.build_conn()}