diff --git a/.env.example b/.env.example
index 543579c..e24b118 100644
--- a/.env.example
+++ b/.env.example
@@ -31,6 +31,10 @@ ASSOCIATION_NAME="Sportsclub XYZ"
# OIDC_ADMIN_GROUP_NAME=admin
# OIDC_GROUPS_CLAIM=groups
+# Optional: Show only OIDC sign-in on login page (hide password form).
+# When set to true and OIDC is configured, users see only the Single Sign-On button.
+# OIDC_ONLY=true
+
# Optional: Vereinfacht accounting integration (finance-contacts sync)
# If set, these override values from Settings UI; those fields become read-only.
# VEREINFACHT_API_URL=https://api.verein.visuel.dev/api/v1
diff --git a/assets/css/app.css b/assets/css/app.css
index 21b1b25..bbe7424 100644
--- a/assets/css/app.css
+++ b/assets/css/app.css
@@ -369,4 +369,24 @@
left: 0 !important;
}
+/* Sign-in: hide SSO button and "or" divider when OIDC is not configured.
+ Scoped to #sign-in-page to avoid hiding unrelated elements. */
+#sign-in-page[data-oidc-configured="false"] [id*="oidc"] {
+ display: none !important;
+}
+#sign-in-page[data-oidc-configured="false"] a[href*="oidc"] {
+ display: none !important;
+}
+#sign-in-page[data-oidc-configured="false"] .divider {
+ display: none !important;
+}
+
+/* Sign-in: when OIDC-only mode is on, hide password form and "or" divider (show only SSO). */
+#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] [id*="password"] {
+ display: none !important;
+}
+#sign-in-page[data-oidc-configured="true"][data-oidc-only="true"] .divider {
+ display: none !important;
+}
+
/* This file is for your main application CSS */
diff --git a/config/dev.exs b/config/dev.exs
index e7b2af8..139b816 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -93,11 +93,13 @@ config :mv, :secret_key_base, "ryn7D6ssmIHQFWIks2sFiTGATgwwAR1+3bN8p7fy6qVtB8qnx
# Signing Secret for Authentication
config :mv, :token_signing_secret, "IwUwi65TrEeExwBXXFPGm2I7889NsL"
-config :mv, :oidc,
- 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/oidc/callback"
+# OIDC: only use when ENV (or Settings) are set. When all OIDC ENVs are commented out,
+# do not set defaults here so the SSO button stays hidden and no MissingSecret occurs.
+# config :mv, :oidc,
+# 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/oidc/callback"
# AshAuthentication development configuration
config :mv, :session_identifier, :jti
diff --git a/config/test.exs b/config/test.exs
index fe2b855..864222f 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -49,6 +49,9 @@ config :mv, :session_identifier, :unsafe
config :mv, :require_token_presence_for_authentication, false
+# Use English as default locale in tests so UI tests can assert on English strings.
+config :mv, :default_locale, "en"
+
# Enable SQL Sandbox for async LiveView tests
# This flag controls sync vs async behavior in CycleGenerator after_action hooks
config :mv, :sql_sandbox, true
diff --git a/docs/admin-bootstrap-and-oidc-role-sync.md b/docs/admin-bootstrap-and-oidc-role-sync.md
index ef7c4ce..abbd03f 100644
--- a/docs/admin-bootstrap-and-oidc-role-sync.md
+++ b/docs/admin-bootstrap-and-oidc-role-sync.md
@@ -33,6 +33,10 @@
- `OIDC_GROUPS_CLAIM` – JWT claim name for group list (default "groups").
- Module: Mv.OidcRoleSyncConfig (oidc_admin_group_name/0, oidc_groups_claim/0).
+### Sign-in page (OIDC-only mode)
+
+- `OIDC_ONLY` (or Settings → OIDC → "Only OIDC sign-in") – When set to true/1/yes and OIDC is configured, the sign-in page shows only the Single Sign-On button (password login is hidden). ENV takes precedence over Settings.
+
### Sync Logic
- Mv.OidcRoleSync.apply_admin_role_from_user_info(user, user_info) – If admin group configured, sets user role to Admin or Mitglied based on user_info groups.
diff --git a/lib/membership/setting.ex b/lib/membership/setting.ex
index 154288b..894725f 100644
--- a/lib/membership/setting.ex
+++ b/lib/membership/setting.ex
@@ -79,7 +79,14 @@ defmodule Mv.Membership.Setting do
:vereinfacht_api_url,
:vereinfacht_api_key,
:vereinfacht_club_id,
- :vereinfacht_app_url
+ :vereinfacht_app_url,
+ :oidc_client_id,
+ :oidc_base_url,
+ :oidc_redirect_uri,
+ :oidc_client_secret,
+ :oidc_admin_group_name,
+ :oidc_groups_claim,
+ :oidc_only
]
end
@@ -96,7 +103,14 @@ defmodule Mv.Membership.Setting do
:vereinfacht_api_url,
:vereinfacht_api_key,
:vereinfacht_club_id,
- :vereinfacht_app_url
+ :vereinfacht_app_url,
+ :oidc_client_id,
+ :oidc_base_url,
+ :oidc_redirect_uri,
+ :oidc_client_secret,
+ :oidc_admin_group_name,
+ :oidc_groups_claim,
+ :oidc_only
]
end
@@ -322,6 +336,52 @@ defmodule Mv.Membership.Setting do
description "Vereinfacht app base URL for contact view links (e.g. https://app.verein.visuel.dev)"
end
+ # OIDC authentication (can be overridden by ENV)
+ attribute :oidc_client_id, :string do
+ allow_nil? true
+ public? true
+ description "OIDC client ID (e.g. from OIDC_CLIENT_ID)"
+ end
+
+ attribute :oidc_base_url, :string do
+ allow_nil? true
+ public? true
+ description "OIDC provider base URL (e.g. from OIDC_BASE_URL)"
+ end
+
+ attribute :oidc_redirect_uri, :string do
+ allow_nil? true
+ public? true
+ description "OIDC redirect URI for callback (e.g. from OIDC_REDIRECT_URI)"
+ end
+
+ attribute :oidc_client_secret, :string do
+ allow_nil? true
+ public? false
+ description "OIDC client secret (e.g. from OIDC_CLIENT_SECRET)"
+ sensitive? true
+ end
+
+ attribute :oidc_admin_group_name, :string do
+ allow_nil? true
+ public? true
+ description "OIDC group name that maps to Admin role (e.g. from OIDC_ADMIN_GROUP_NAME)"
+ end
+
+ attribute :oidc_groups_claim, :string do
+ allow_nil? true
+ public? true
+ description "JWT claim name for group list (e.g. from OIDC_GROUPS_CLAIM, default 'groups')"
+ end
+
+ attribute :oidc_only, :boolean do
+ allow_nil? false
+ default false
+ public? true
+
+ description "When true and OIDC is configured, sign-in shows only OIDC (password login hidden)"
+ end
+
timestamps()
end
diff --git a/lib/mv/config.ex b/lib/mv/config.ex
index d2ad66c..ec69b18 100644
--- a/lib/mv/config.ex
+++ b/lib/mv/config.ex
@@ -262,13 +262,44 @@ defmodule Mv.Config do
end
end
+ defp env_or_setting_bool(env_key, setting_key) do
+ case System.get_env(env_key) do
+ nil ->
+ get_from_settings_bool(setting_key)
+
+ value when is_binary(value) ->
+ v = String.trim(value) |> String.downcase()
+ v in ["true", "1", "yes"]
+
+ _ ->
+ false
+ end
+ end
+
defp get_vereinfacht_from_settings(key) do
+ get_from_settings(key)
+ end
+
+ defp get_from_settings(key) do
case Mv.Membership.get_settings() do
{:ok, settings} -> settings |> Map.get(key) |> trim_nil()
{:error, _} -> nil
end
end
+ defp get_from_settings_bool(key) do
+ case Mv.Membership.get_settings() do
+ {:ok, settings} ->
+ case Map.get(settings, key) do
+ true -> true
+ _ -> false
+ end
+
+ {:error, _} ->
+ false
+ end
+ end
+
defp trim_nil(nil), do: nil
defp trim_nil(s) when is_binary(s) do
@@ -298,4 +329,105 @@ defmodule Mv.Config do
defp present?(nil), do: false
defp present?(s) when is_binary(s), do: String.trim(s) != ""
defp present?(_), do: false
+
+ # ---------------------------------------------------------------------------
+ # OIDC authentication
+ # ENV variables take priority; fallback to Settings from database.
+ # ---------------------------------------------------------------------------
+
+ @doc """
+ Returns the OIDC client ID. ENV first, then Settings.
+ """
+ @spec oidc_client_id() :: String.t() | nil
+ def oidc_client_id do
+ env_or_setting("OIDC_CLIENT_ID", :oidc_client_id)
+ end
+
+ @doc """
+ Returns the OIDC provider base URL. ENV first, then Settings.
+ """
+ @spec oidc_base_url() :: String.t() | nil
+ def oidc_base_url do
+ env_or_setting("OIDC_BASE_URL", :oidc_base_url)
+ end
+
+ @doc """
+ Returns the OIDC redirect URI. ENV first, then Settings.
+ """
+ @spec oidc_redirect_uri() :: String.t() | nil
+ def oidc_redirect_uri do
+ env_or_setting("OIDC_REDIRECT_URI", :oidc_redirect_uri)
+ end
+
+ @doc """
+ Returns the OIDC client secret. ENV first, then Settings.
+ """
+ @spec oidc_client_secret() :: String.t() | nil
+ def oidc_client_secret do
+ env_or_setting("OIDC_CLIENT_SECRET", :oidc_client_secret)
+ end
+
+ @doc """
+ Returns the OIDC admin group name (for role sync). ENV first, then Settings.
+ """
+ @spec oidc_admin_group_name() :: String.t() | nil
+ def oidc_admin_group_name do
+ env_or_setting("OIDC_ADMIN_GROUP_NAME", :oidc_admin_group_name)
+ end
+
+ @doc """
+ Returns the OIDC groups claim name (default "groups"). ENV first, then Settings.
+ """
+ @spec oidc_groups_claim() :: String.t() | nil
+ def oidc_groups_claim do
+ case env_or_setting("OIDC_GROUPS_CLAIM", :oidc_groups_claim) do
+ nil -> "groups"
+ v -> v
+ end
+ end
+
+ @doc """
+ Returns true if any OIDC ENV variable is set (used to show hint in Settings UI).
+ """
+ @spec oidc_env_configured?() :: boolean()
+ def oidc_env_configured? do
+ oidc_client_id_env_set?() or oidc_base_url_env_set?() or
+ oidc_redirect_uri_env_set?() or oidc_client_secret_env_set?() or
+ oidc_admin_group_name_env_set?() or oidc_groups_claim_env_set?() or
+ oidc_only_env_set?()
+ end
+
+ @doc """
+ Returns true when OIDC is configured and can be used for sign-in (client ID, base URL,
+ redirect URI, and client secret must be set). Used to show or hide the Single Sign-On button on the
+ sign-in page. Without client secret, the OIDC flow fails with MissingSecret; without redirect_uri,
+ the OIDC Plug crashes with URI.new(nil).
+ """
+ @spec oidc_configured?() :: boolean()
+ def oidc_configured? do
+ id = oidc_client_id()
+ base = oidc_base_url()
+ secret = oidc_client_secret()
+ redirect = oidc_redirect_uri()
+ present = &(is_binary(&1) and String.trim(&1) != "")
+ present.(id) and present.(base) and present.(secret) and present.(redirect)
+ end
+
+ @doc """
+ Returns true when only OIDC sign-in should be shown (password login hidden).
+ ENV OIDC_ONLY first (true/1/yes vs false/0/no), then Settings.oidc_only.
+ Only has effect when OIDC is configured; when false or OIDC not configured, both password and OIDC are shown as usual.
+ """
+ @spec oidc_only?() :: boolean()
+ def oidc_only? do
+ env_or_setting_bool("OIDC_ONLY", :oidc_only)
+ end
+
+ def oidc_client_id_env_set?, do: env_set?("OIDC_CLIENT_ID")
+ def oidc_base_url_env_set?, do: env_set?("OIDC_BASE_URL")
+ def oidc_redirect_uri_env_set?, do: env_set?("OIDC_REDIRECT_URI")
+ def oidc_client_secret_env_set?, do: env_set?("OIDC_CLIENT_SECRET")
+ def oidc_admin_group_name_env_set?, do: env_set?("OIDC_ADMIN_GROUP_NAME")
+ def oidc_groups_claim_env_set?, do: env_set?("OIDC_GROUPS_CLAIM")
+ def oidc_only_env_set?, do: env_set?("OIDC_ONLY")
end
diff --git a/lib/mv/oidc_role_sync_config.ex b/lib/mv/oidc_role_sync_config.ex
index 493a435..2a8574c 100644
--- a/lib/mv/oidc_role_sync_config.ex
+++ b/lib/mv/oidc_role_sync_config.ex
@@ -2,23 +2,19 @@ defmodule Mv.OidcRoleSyncConfig do
@moduledoc """
Runtime configuration for OIDC group → role sync (e.g. admin group → Admin role).
- Reads from Application config `:mv, :oidc_role_sync`:
- - `:admin_group_name` – OIDC group name that maps to Admin role (optional; when nil, no sync).
- - `:groups_claim` – JWT/user_info claim name for groups (default: `"groups"`).
+ Reads from Mv.Config (ENV first, then Settings):
+ - `oidc_admin_group_name/0` – OIDC group name that maps to Admin role (optional; when nil, no sync).
+ - `oidc_groups_claim/0` – JWT/user_info claim name for groups (default: `"groups"`).
- Set via ENV in production: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM (see config/runtime.exs).
+ Set via ENV: OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM; or via Settings (Basic settings → OIDC).
"""
@doc "Returns the OIDC group name that maps to Admin role, or nil if not configured."
def oidc_admin_group_name do
- get(:admin_group_name)
+ Mv.Config.oidc_admin_group_name()
end
@doc "Returns the JWT/user_info claim name for groups; defaults to \"groups\"."
def oidc_groups_claim do
- get(:groups_claim) || "groups"
- end
-
- defp get(key) do
- Application.get_env(:mv, :oidc_role_sync, []) |> Keyword.get(key)
+ Mv.Config.oidc_groups_claim() || "groups"
end
end
diff --git a/lib/mv/secrets.ex b/lib/mv/secrets.ex
index d3bc30d..177ed90 100644
--- a/lib/mv/secrets.ex
+++ b/lib/mv/secrets.ex
@@ -7,59 +7,66 @@ defmodule Mv.Secrets do
particularly for OIDC (Rauthy) authentication.
## Configuration Source
- Secrets are read from the `:oidc` key in the application configuration,
- which is typically set in `config/runtime.exs` from environment variables:
- - `OIDC_CLIENT_ID`
- - `OIDC_CLIENT_SECRET`
- - `OIDC_BASE_URL`
- - `OIDC_REDIRECT_URI`
+ Secrets are read via `Mv.Config` which prefers environment variables and
+ falls back to Settings from the database:
+ - OIDC_CLIENT_ID / settings.oidc_client_id
+ - OIDC_CLIENT_SECRET / settings.oidc_client_secret
+ - OIDC_BASE_URL / settings.oidc_base_url
+ - OIDC_REDIRECT_URI / settings.oidc_redirect_uri
- ## Usage
- This module is automatically called by AshAuthentication when resolving
- secrets for the User resource's OIDC strategy.
+ When a value is nil, returns `{:error, MissingSecret}` so that AshAuthentication
+ does not crash (e.g. URI.new(nil)) and can redirect to sign-in with an error.
"""
use AshAuthentication.Secret
+ alias AshAuthentication.Errors.MissingSecret
+
def secret_for(
[:authentication, :strategies, :oidc, :client_id],
- Mv.Accounts.User,
+ resource,
_opts,
_meth
) do
- get_config(:client_id)
+ secret_or_error(Mv.Config.oidc_client_id(), resource, :client_id)
end
def secret_for(
[:authentication, :strategies, :oidc, :redirect_uri],
- Mv.Accounts.User,
+ resource,
_opts,
_meth
) do
- get_config(:redirect_uri)
+ secret_or_error(Mv.Config.oidc_redirect_uri(), resource, :redirect_uri)
end
def secret_for(
[:authentication, :strategies, :oidc, :client_secret],
- Mv.Accounts.User,
+ resource,
_opts,
_meth
) do
- get_config(:client_secret)
+ secret_or_error(Mv.Config.oidc_client_secret(), resource, :client_secret)
end
def secret_for(
[:authentication, :strategies, :oidc, :base_url],
- Mv.Accounts.User,
+ resource,
_opts,
_meth
) do
- get_config(:base_url)
+ secret_or_error(Mv.Config.oidc_base_url(), resource, :base_url)
end
- defp get_config(key) do
- :mv
- |> Application.fetch_env!(:oidc)
- |> Keyword.fetch!(key)
- |> then(&{:ok, &1})
+ defp secret_or_error(nil, resource, key) do
+ path = [:authentication, :strategies, :oidc, key]
+ {:error, MissingSecret.exception(path: path, resource: resource)}
+ end
+
+ defp secret_or_error(value, resource, key) when is_binary(value) do
+ if String.trim(value) == "" do
+ secret_or_error(nil, resource, key)
+ else
+ {:ok, value}
+ end
end
end
diff --git a/lib/mv/vereinfacht/client.ex b/lib/mv/vereinfacht/client.ex
index 58e06a9..6ec8c8c 100644
--- a/lib/mv/vereinfacht/client.ex
+++ b/lib/mv/vereinfacht/client.ex
@@ -10,6 +10,54 @@ defmodule Mv.Vereinfacht.Client do
@content_type "application/vnd.api+json"
+ @doc """
+ Tests the connection to the Vereinfacht API with the given credentials.
+
+ Makes a lightweight `GET /finance-contacts?page[size]=1` request to verify
+ that the API URL, API key, and club ID are valid and reachable.
+
+ ## Returns
+ - `{:ok, :connected}` – credentials are valid (HTTP 200)
+ - `{:error, :not_configured}` – any parameter is nil or blank
+ - `{:error, {:http, status, message}}` – API returned an error (e.g. 401, 403)
+ - `{:error, {:request_failed, reason}}` – network/transport error
+
+ ## Examples
+
+ iex> test_connection("https://api.example.com/api/v1", "token", "2")
+ {:ok, :connected}
+
+ iex> test_connection(nil, "token", "2")
+ {:error, :not_configured}
+ """
+ @spec test_connection(String.t() | nil, String.t() | nil, String.t() | nil) ::
+ {:ok, :connected} | {:error, term()}
+ def test_connection(api_url, api_key, club_id) do
+ if blank?(api_url) or blank?(api_key) or blank?(club_id) do
+ {:error, :not_configured}
+ else
+ url =
+ api_url
+ |> String.trim_trailing("/")
+ |> then(&"#{&1}/finance-contacts?page[size]=1")
+
+ case Req.get(url, [headers: headers(api_key)] ++ req_http_options()) do
+ {:ok, %{status: 200}} ->
+ {:ok, :connected}
+
+ {:ok, %{status: status, body: body}} ->
+ {:error, {:http, status, extract_error_message(body)}}
+
+ {:error, reason} ->
+ {:error, {:request_failed, reason}}
+ end
+ end
+ end
+
+ defp blank?(nil), do: true
+ defp blank?(s) when is_binary(s), do: String.trim(s) == ""
+ defp blank?(_), do: true
+
@doc """
Creates a finance contact in Vereinfacht for the given member.
@@ -360,5 +408,16 @@ defmodule Mv.Vereinfacht.Client do
defp extract_error_message(%{"errors" => [%{"detail" => d} | _]}) when is_binary(d), do: d
defp extract_error_message(%{"errors" => [%{"title" => t} | _]}) when is_binary(t), do: t
defp extract_error_message(body) when is_map(body), do: inspect(body)
+
+ defp extract_error_message(body) when is_binary(body) do
+ trimmed = String.trim(body)
+
+ if String.starts_with?(trimmed, "<") do
+ :html_response
+ else
+ trimmed
+ end
+ end
+
defp extract_error_message(other), do: inspect(other)
end
diff --git a/lib/mv/vereinfacht/vereinfacht.ex b/lib/mv/vereinfacht/vereinfacht.ex
index ce8005d..6520b64 100644
--- a/lib/mv/vereinfacht/vereinfacht.ex
+++ b/lib/mv/vereinfacht/vereinfacht.ex
@@ -14,6 +14,27 @@ defmodule Mv.Vereinfacht do
alias Mv.Helpers.SystemActor
alias Mv.Helpers
+ @doc """
+ Tests the connection to the Vereinfacht API using the current configuration.
+
+ Delegates to `Mv.Vereinfacht.Client.test_connection/3` with the values from
+ `Mv.Config` (ENV variables take priority over database settings).
+
+ ## Returns
+ - `{:ok, :connected}` – credentials are valid and API is reachable
+ - `{:error, :not_configured}` – URL, API key or club ID is missing
+ - `{:error, {:http, status, message}}` – API returned an error (e.g. 401, 403)
+ - `{:error, {:request_failed, reason}}` – network/transport error
+ """
+ @spec test_connection() :: {:ok, :connected} | {:error, term()}
+ def test_connection do
+ Client.test_connection(
+ Mv.Config.vereinfacht_api_url(),
+ Mv.Config.vereinfacht_api_key(),
+ Mv.Config.vereinfacht_club_id()
+ )
+ end
+
@doc """
Syncs a single member to Vereinfacht (create or update finance contact).
diff --git a/lib/mv_web/auth_overrides.ex b/lib/mv_web/auth_overrides.ex
index b121c4e..f28d81f 100644
--- a/lib/mv_web/auth_overrides.ex
+++ b/lib/mv_web/auth_overrides.ex
@@ -38,12 +38,10 @@ defmodule MvWeb.AuthOverrides do
set :image_url, nil
end
- # Translate the or in the horizontal rule to German
+ # Translate the "or" in the horizontal rule (between password form and SSO).
+ # Uses auth domain so it respects the current locale (e.g. "oder" in German).
override AshAuthentication.Phoenix.Components.HorizontalRule do
- set :text,
- Gettext.with_locale(MvWeb.Gettext, "de", fn ->
- Gettext.gettext(MvWeb.Gettext, "or")
- end)
+ set :text, dgettext("auth", "or")
end
# Hide AshAuthentication's Flash component since we use flash_group in root layout
diff --git a/lib/mv_web/components/layouts/sidebar.ex b/lib/mv_web/components/layouts/sidebar.ex
index 7d4cce6..cb94fb3 100644
--- a/lib/mv_web/components/layouts/sidebar.ex
+++ b/lib/mv_web/components/layouts/sidebar.ex
@@ -80,11 +80,11 @@ defmodule MvWeb.Layouts.Sidebar do
/>
<% end %>
- <%= if can_access_page?(@current_user, PagePaths.membership_fee_types()) do %>
+ <%= if can_access_page?(@current_user, PagePaths.groups()) do %>
<.menu_item
- href={~p"/membership_fee_types"}
- icon="hero-currency-euro"
- label={gettext("Fee Types")}
+ href={~p"/groups"}
+ icon="hero-user-group"
+ label={gettext("Groups")}
/>
<% end %>
@@ -102,24 +102,26 @@ defmodule MvWeb.Layouts.Sidebar do
label={gettext("Administration")}
testid="sidebar-administration"
>
- <%= if can_access_page?(@current_user, PagePaths.users()) do %>
- <.menu_subitem href={~p"/users"} label={gettext("Users")} />
+ <%= if can_access_page?(@current_user, PagePaths.settings()) do %>
+ <.menu_subitem href={~p"/settings"} label={gettext("Basic settings")} />
<% end %>
- <%= if can_access_page?(@current_user, PagePaths.groups()) do %>
- <.menu_subitem href={~p"/groups"} label={gettext("Groups")} />
- <% end %>
- <%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
- <.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
+ <%= if can_access_page?(@current_user, PagePaths.admin_datafields()) do %>
+ <.menu_subitem href={~p"/admin/datafields"} label={gettext("Datafields")} />
<% end %>
<%= if can_access_page?(@current_user, PagePaths.membership_fee_settings()) do %>
<.menu_subitem
href={~p"/membership_fee_settings"}
- label={gettext("Fee Settings")}
+ label={gettext("Membership fee settings")}
/>
<% end %>
- <%= if can_access_page?(@current_user, PagePaths.settings()) do %>
+ <%= if can_access_page?(@current_user, PagePaths.admin_import()) do %>
<.menu_subitem href={~p"/admin/import"} label={gettext("Import")} />
- <.menu_subitem href={~p"/settings"} label={gettext("Settings")} />
+ <% end %>
+ <%= if can_access_page?(@current_user, PagePaths.users()) do %>
+ <.menu_subitem href={~p"/users"} label={gettext("Users")} />
+ <% end %>
+ <%= if can_access_page?(@current_user, PagePaths.admin_roles()) do %>
+ <.menu_subitem href={~p"/admin/roles"} label={gettext("Roles")} />
<% end %>
<% end %>
diff --git a/lib/mv_web/live/auth/sign_in_live.ex b/lib/mv_web/live/auth/sign_in_live.ex
new file mode 100644
index 0000000..aa0d640
--- /dev/null
+++ b/lib/mv_web/live/auth/sign_in_live.ex
@@ -0,0 +1,101 @@
+defmodule MvWeb.SignInLive do
+ @moduledoc """
+ Custom sign-in page with language selector and conditional Single Sign-On button.
+
+ - Renders a language selector (same pattern as LinkOidcAccountLive).
+ - Wraps the default AshAuthentication SignIn component in a container with
+ `data-oidc-configured` so that CSS can hide the SSO button when OIDC is not configured.
+ """
+ use Phoenix.LiveView
+ use Gettext, backend: MvWeb.Gettext
+
+ alias AshAuthentication.Phoenix.Components
+ alias Mv.Config
+
+ @impl true
+ def mount(_params, session, socket) do
+ overrides =
+ session
+ |> Map.get("overrides", [AshAuthentication.Phoenix.Overrides.Default])
+
+ # Locale: same fallback as LiveUserAuth so config :default_locale (e.g. "en" in test) is respected
+ locale =
+ session["locale"] || Application.get_env(:mv, :default_locale, "de")
+
+ Gettext.put_locale(MvWeb.Gettext, locale)
+
+ socket =
+ socket
+ |> assign(overrides: overrides)
+ |> assign_new(:otp_app, fn -> nil end)
+ |> assign(:path, session["path"] || "/")
+ |> assign(:reset_path, session["reset_path"])
+ |> assign(:register_path, session["register_path"])
+ |> assign(:current_tenant, session["tenant"])
+ |> assign(:resources, session["resources"])
+ |> assign(:context, session["context"] || %{})
+ |> assign(:auth_routes_prefix, session["auth_routes_prefix"])
+ |> assign(:gettext_fn, session["gettext_fn"])
+ |> assign(:live_action, :sign_in)
+ |> assign(:oidc_configured, Config.oidc_configured?())
+ |> assign(:oidc_only, Config.oidc_only?())
+ |> assign(:root_class, "grid h-screen place-items-center bg-base-100")
+ |> assign(:sign_in_id, "sign-in")
+ |> assign(:locale, locale)
+
+ {:ok, socket}
+ end
+
+ @impl true
+ def handle_params(_, _uri, socket) do
+ {:noreply, socket}
+ end
+
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ {gettext(
+ "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
+ )}
+
+
+
+ <.button
+ :if={
+ not (@oidc_client_id_env_set and @oidc_base_url_env_set and
+ @oidc_redirect_uri_env_set and @oidc_client_secret_env_set and
+ @oidc_admin_group_name_env_set and @oidc_groups_claim_env_set and
+ @oidc_only_env_set)
+ }
+ phx-disable-with={gettext("Saving...")}
+ variant="primary"
+ class="mt-2"
+ >
+ {gettext("Save OIDC Settings")}
+
+
"""
@@ -207,6 +344,12 @@ defmodule MvWeb.GlobalSettingsLive do
assign(socket, form: AshPhoenix.Form.validate(socket.assigns.form, setting_params))}
end
+ @impl true
+ def handle_event("test_vereinfacht_connection", _params, socket) do
+ result = Mv.Vereinfacht.test_connection()
+ {:noreply, assign(socket, :vereinfacht_test_result, result)}
+ end
+
@impl true
def handle_event("sync_vereinfacht_contacts", _params, socket) do
case Mv.Vereinfacht.sync_members_without_contact() do
@@ -244,17 +387,28 @@ defmodule MvWeb.GlobalSettingsLive do
@impl true
def handle_event("save", %{"setting" => setting_params}, socket) do
actor = MvWeb.LiveHelpers.current_actor(socket)
- # Never send blank API key so we do not overwrite the stored secret (security)
- setting_params_clean = drop_blank_vereinfacht_api_key(setting_params)
+ # Never send blank API key / client secret so we do not overwrite stored secrets
+ setting_params_clean =
+ setting_params
+ |> drop_blank_vereinfacht_api_key()
+ |> drop_blank_oidc_client_secret()
+
+ saves_vereinfacht = vereinfacht_params?(setting_params_clean)
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
{:ok, _updated_settings} ->
{:ok, fresh_settings} = Membership.get_settings()
+ test_result =
+ if saves_vereinfacht, do: Mv.Vereinfacht.test_connection(), else: nil
+
socket =
socket
|> assign(:settings, fresh_settings)
|> assign(:vereinfacht_api_key_set, present?(fresh_settings.vereinfacht_api_key))
+ |> assign(:oidc_client_secret_set, present?(fresh_settings.oidc_client_secret))
+ |> assign(:oidc_configured, Mv.Config.oidc_configured?())
+ |> assign(:vereinfacht_test_result, test_result)
|> put_flash(:info, gettext("Settings updated successfully"))
|> assign_form()
@@ -265,6 +419,12 @@ defmodule MvWeb.GlobalSettingsLive do
end
end
+ @vereinfacht_param_keys ~w[vereinfacht_api_url vereinfacht_api_key vereinfacht_club_id vereinfacht_app_url]
+
+ defp vereinfacht_params?(params) when is_map(params) do
+ Enum.any?(@vereinfacht_param_keys, &Map.has_key?(params, &1))
+ end
+
defp drop_blank_vereinfacht_api_key(params) when is_map(params) do
case params do
%{"vereinfacht_api_key" => v} when v in [nil, ""] ->
@@ -275,88 +435,28 @@ defmodule MvWeb.GlobalSettingsLive do
end
end
- @impl true
- def handle_info({:custom_field_saved, _custom_field, action}, socket) do
- send_update(MvWeb.CustomFieldLive.IndexComponent,
- id: "custom-fields-component",
- show_form: false
- )
+ defp drop_blank_oidc_client_secret(params) when is_map(params) do
+ case params do
+ %{"oidc_client_secret" => v} when v in [nil, ""] ->
+ Map.delete(params, "oidc_client_secret")
- {:noreply,
- socket
- |> assign(:active_editing_section, nil)
- |> put_flash(:info, gettext("Data field %{action} successfully", action: action))}
- end
-
- @impl true
- def handle_info({:custom_field_deleted, _custom_field}, socket) do
- {:noreply, put_flash(socket, :info, gettext("Data field deleted successfully"))}
- end
-
- @impl true
- def handle_info({:custom_field_delete_error, error}, socket) do
- {:noreply,
- put_flash(
- socket,
- :error,
- gettext("Failed to delete data field: %{error}", error: inspect(error))
- )}
- end
-
- @impl true
- def handle_info(:custom_field_slug_mismatch, socket) do
- {:noreply, put_flash(socket, :error, gettext("Slug does not match. Deletion cancelled."))}
- end
-
- def handle_info({:custom_fields_load_error, _error}, socket) do
- {:noreply,
- put_flash(
- socket,
- :error,
- gettext("Could not load data fields. Please check your permissions.")
- )}
- end
-
- @impl true
- def handle_info({:editing_section_changed, section}, socket) do
- {:noreply, assign(socket, :active_editing_section, section)}
- end
-
- @impl true
- def handle_info({:member_field_saved, _member_field, action}, socket) do
- # Reload settings to get updated member_field_visibility
- {:ok, updated_settings} = Membership.get_settings()
-
- # Send update to member fields component to close form
- send_update(MvWeb.MemberFieldLive.IndexComponent,
- id: "member-fields-component",
- show_form: false,
- settings: updated_settings
- )
-
- {:noreply,
- socket
- |> assign(:settings, updated_settings)
- |> assign(:active_editing_section, nil)
- |> put_flash(:info, gettext("Member field %{action} successfully", action: action))}
- end
-
- @impl true
- def handle_info({:member_field_visibility_updated}, socket) do
- # Legacy event - reload settings and update component
- {:ok, updated_settings} = Membership.get_settings()
-
- send_update(MvWeb.MemberFieldLive.IndexComponent,
- id: "member-fields-component",
- settings: updated_settings
- )
-
- {:noreply, assign(socket, :settings, updated_settings)}
+ _ ->
+ params
+ end
end
defp assign_form(%{assigns: %{settings: settings}} = socket) do
- # Never put API key into form/DOM to avoid secret leak in source or DevTools
- settings_for_form = %{settings | vereinfacht_api_key: nil}
+ # Show ENV values in disabled fields (Vereinfacht and OIDC); never expose API key / client secret
+ settings_display =
+ settings
+ |> merge_vereinfacht_env_values()
+ |> merge_oidc_env_values()
+
+ settings_for_form = %{
+ settings_display
+ | vereinfacht_api_key: nil,
+ oidc_client_secret: nil
+ }
form =
AshPhoenix.Form.for_update(
@@ -370,6 +470,66 @@ defmodule MvWeb.GlobalSettingsLive do
assign(socket, form: to_form(form))
end
+ defp put_if_env_set(map, _key, false, _value), do: map
+ defp put_if_env_set(map, key, true, value), do: Map.put(map, key, value)
+
+ defp merge_vereinfacht_env_values(s) do
+ s
+ |> put_if_env_set(
+ :vereinfacht_api_url,
+ Mv.Config.vereinfacht_api_url_env_set?(),
+ Mv.Config.vereinfacht_api_url()
+ )
+ |> put_if_env_set(
+ :vereinfacht_club_id,
+ Mv.Config.vereinfacht_club_id_env_set?(),
+ Mv.Config.vereinfacht_club_id()
+ )
+ |> put_if_env_set(
+ :vereinfacht_app_url,
+ Mv.Config.vereinfacht_app_url_env_set?(),
+ Mv.Config.vereinfacht_app_url()
+ )
+ end
+
+ defp merge_oidc_env_values(s) do
+ s
+ |> put_if_env_set(
+ :oidc_client_id,
+ Mv.Config.oidc_client_id_env_set?(),
+ Mv.Config.oidc_client_id()
+ )
+ |> put_if_env_set(
+ :oidc_base_url,
+ Mv.Config.oidc_base_url_env_set?(),
+ Mv.Config.oidc_base_url()
+ )
+ |> put_if_env_set(
+ :oidc_redirect_uri,
+ Mv.Config.oidc_redirect_uri_env_set?(),
+ Mv.Config.oidc_redirect_uri()
+ )
+ |> put_if_env_set(
+ :oidc_admin_group_name,
+ Mv.Config.oidc_admin_group_name_env_set?(),
+ Mv.Config.oidc_admin_group_name()
+ )
+ |> put_if_env_set(
+ :oidc_groups_claim,
+ Mv.Config.oidc_groups_claim_env_set?(),
+ Mv.Config.oidc_groups_claim()
+ )
+ |> put_if_oidc_only_env_set()
+ end
+
+ defp put_if_oidc_only_env_set(s) do
+ if Mv.Config.oidc_only_env_set?() do
+ Map.put(s, :oidc_only, Mv.Config.oidc_only?())
+ else
+ s
+ end
+ end
+
defp enrich_sync_errors([]), do: []
defp enrich_sync_errors(errors) when is_list(errors) do
@@ -412,6 +572,109 @@ defmodule MvWeb.GlobalSettingsLive do
Gettext.dgettext(MvWeb.Gettext, "default", message)
end
+ attr :result, :any, required: true
+
+ defp vereinfacht_test_result(%{result: {:ok, :connected}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-check-circle" class="size-5 shrink-0" />
+ {gettext("Connection successful. API URL, API Key and Club ID are valid.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, :not_configured}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-exclamation-triangle" class="size-5 shrink-0" />
+ {gettext("Not configured. Please set API URL, API Key and Club ID.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, _status, :html_response}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+ )}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 401, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+ {gettext("Connection failed (HTTP 401): API key is invalid or missing.")}
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 403, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+ )}
+
+
+ """
+ end
+
+ defp vereinfacht_test_result(%{result: {:error, {:http, 404, _}}} = assigns) do
+ ~H"""
+
+ <.icon name="hero-x-circle" class="size-5 shrink-0 mt-0.5" />
+
+ {gettext(
+ "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+ )}
+
+
+ {gettext(
+ "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
+ )}
+
+
+
+ {gettext("Name & Amount")}
+ - {gettext("Can be changed at any time. Amount changes affect future periods only.")}
+
+
+ {gettext("Interval")}
+ - {gettext(
+ "Fixed after creation. Members can only switch between types with the same interval."
+ )}
+
+
+ {gettext("Deletion")}
+ - {gettext("Only possible if no members are assigned to this type.")}
+
+
+
+
+
"""
end
@@ -286,6 +456,32 @@ defmodule MvWeb.MembershipFeeSettingsLive do
defp format_interval(:half_yearly), do: gettext("Half-yearly")
defp format_interval(:yearly), do: gettext("Yearly")
+ defp load_member_counts(fee_types, actor) do
+ fee_type_ids = Enum.map(fee_types, & &1.id)
+
+ members =
+ Member
+ |> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids)
+ |> Ash.Query.select([:membership_fee_type_id])
+ |> Ash.read!(domain: Membership, actor: actor)
+
+ members
+ |> Enum.group_by(& &1.membership_fee_type_id)
+ |> Enum.map(fn {fee_type_id, members_list} -> {fee_type_id, length(members_list)} end)
+ |> Map.new()
+ end
+
+ defp get_member_count(fee_type, member_counts) do
+ Map.get(member_counts, fee_type.id, 0)
+ end
+
+ defp format_error(%Ash.Error.Invalid{} = error) do
+ Enum.map_join(error.errors, ", ", fn e -> e.message end)
+ end
+
+ defp format_error(error) when is_binary(error), do: error
+ defp format_error(_error), do: gettext("An error occurred")
+
defp assign_form(%{assigns: %{settings: settings}} = socket) do
form =
AshPhoenix.Form.for_update(
diff --git a/lib/mv_web/live/membership_fee_type_live/form.ex b/lib/mv_web/live/membership_fee_type_live/form.ex
index 6fe80a8..d8569e2 100644
--- a/lib/mv_web/live/membership_fee_type_live/form.ex
+++ b/lib/mv_web/live/membership_fee_type_live/form.ex
@@ -384,7 +384,8 @@ defmodule MvWeb.MembershipFeeTypeLive.Form do
defp format_interval_value(value), do: to_string(value)
@spec return_path(String.t(), MembershipFeeType.t() | nil) :: String.t()
- defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_types"
+ defp return_path("index", _membership_fee_type), do: ~p"/membership_fee_settings"
+ defp return_path(_, _), do: ~p"/membership_fee_settings"
@spec get_affected_member_count(String.t(), Mv.Accounts.User.t() | nil) :: non_neg_integer()
# Checks if amount changed and updates socket assigns accordingly
diff --git a/lib/mv_web/live/membership_fee_type_live/index.ex b/lib/mv_web/live/membership_fee_type_live/index.ex
index 84cd26d..f5f760f 100644
--- a/lib/mv_web/live/membership_fee_type_live/index.ex
+++ b/lib/mv_web/live/membership_fee_type_live/index.ex
@@ -47,7 +47,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
{gettext("Manage membership fee types for membership fees.")}
<:actions>
- <.button variant="primary" navigate={~p"/membership_fee_types/new"}>
+ <.button variant="primary" navigate={~p"/membership_fee_settings/new_fee_type"}>
<.icon name="hero-plus" /> {gettext("New Membership Fee Type")}
@@ -79,7 +79,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
<:action :let={mft}>
<.link
- navigate={~p"/membership_fee_types/#{mft.id}/edit"}
+ navigate={~p"/membership_fee_settings/#{mft.id}/edit_fee_type"}
class="btn btn-ghost btn-xs"
aria-label={gettext("Edit membership fee type")}
>
diff --git a/lib/mv_web/live_user_auth.ex b/lib/mv_web/live_user_auth.ex
index b78ba21..c913caa 100644
--- a/lib/mv_web/live_user_auth.ex
+++ b/lib/mv_web/live_user_auth.ex
@@ -42,9 +42,8 @@ defmodule MvWeb.LiveUserAuth do
end
def on_mount(:live_no_user, _params, session, socket) do
- # Set the locale for not logged in user to set the language in the Log-In Screen
- # otherwise the locale is not taken for the Log-In Screen
- locale = session["locale"] || "en"
+ # Set the locale for not logged in user (default from config, "de" in dev/prod).
+ locale = session["locale"] || Application.get_env(:mv, :default_locale, "de")
Gettext.put_locale(MvWeb.Gettext, locale)
{:cont, assign(socket, :locale, locale)}
diff --git a/lib/mv_web/locale_controller.ex b/lib/mv_web/locale_controller.ex
index 99a200f..afa1737 100644
--- a/lib/mv_web/locale_controller.ex
+++ b/lib/mv_web/locale_controller.ex
@@ -1,10 +1,11 @@
defmodule MvWeb.LocaleController do
use MvWeb, :controller
- def set_locale(conn, %{"locale" => locale}) do
+ @supported_locales ["de", "en"]
+
+ def set_locale(conn, %{"locale" => locale}) when locale in @supported_locales do
conn
|> put_session(:locale, locale)
- # Store locale in a cookie that persists beyond the session
|> put_resp_cookie("locale", locale,
max_age: 365 * 24 * 60 * 60,
same_site: "Lax",
@@ -14,6 +15,8 @@ defmodule MvWeb.LocaleController do
|> redirect(to: get_referer(conn) || "/")
end
+ def set_locale(conn, _params), do: redirect(conn, to: get_referer(conn) || "/")
+
defp get_referer(conn) do
conn.req_headers
|> Enum.find(fn {k, _v} -> k == "referer" end)
diff --git a/lib/mv_web/page_paths.ex b/lib/mv_web/page_paths.ex
index 2720c0f..551cada 100644
--- a/lib/mv_web/page_paths.ex
+++ b/lib/mv_web/page_paths.ex
@@ -8,30 +8,30 @@ defmodule MvWeb.PagePaths do
# Sidebar top-level menu paths
@members "/members"
- @membership_fee_types "/membership_fee_types"
@statistics "/statistics"
# Administration submenu paths (all must match router)
@users "/users"
@groups "/groups"
@admin_roles "/admin/roles"
+ @admin_datafields "/admin/datafields"
@membership_fee_settings "/membership_fee_settings"
+ @admin_import "/admin/import"
@settings "/settings"
@admin_page_paths [
@users,
@groups,
@admin_roles,
+ @admin_datafields,
@membership_fee_settings,
+ @admin_import,
@settings
]
@doc "Path for Members index (sidebar and page permission check)."
def members, do: @members
- @doc "Path for Membership Fee Types index (sidebar and page permission check)."
- def membership_fee_types, do: @membership_fee_types
-
@doc "Path for Statistics page (sidebar and page permission check)."
def statistics, do: @statistics
@@ -41,6 +41,8 @@ defmodule MvWeb.PagePaths do
def users, do: @users
def groups, do: @groups
def admin_roles, do: @admin_roles
+ def admin_datafields, do: @admin_datafields
def membership_fee_settings, do: @membership_fee_settings
+ def admin_import, do: @admin_import
def settings, do: @settings
end
diff --git a/lib/mv_web/router.ex b/lib/mv_web/router.ex
index ec90f1b..8a4e6c0 100644
--- a/lib/mv_web/router.ex
+++ b/lib/mv_web/router.ex
@@ -68,16 +68,13 @@ defmodule MvWeb.Router do
live "/settings", GlobalSettingsLive
- # Membership Fee Settings
+ # Membership Fee Settings (includes fee types list; new/edit under sub-routes)
live "/membership_fee_settings", MembershipFeeSettingsLive
-
- # Membership Fee Types Management
- live "/membership_fee_types", MembershipFeeTypeLive.Index, :index
+ live "/membership_fee_settings/new_fee_type", MembershipFeeTypeLive.Form, :new
+ live "/membership_fee_settings/:id/edit_fee_type", MembershipFeeTypeLive.Form, :edit
# Statistics
live "/statistics", StatisticsLive, :index
- live "/membership_fee_types/new", MembershipFeeTypeLive.Form, :new
- live "/membership_fee_types/:id/edit", MembershipFeeTypeLive.Form, :edit
# Groups Management
live "/groups", GroupLive.Index, :index
@@ -91,6 +88,9 @@ defmodule MvWeb.Router do
live "/admin/roles/:id", RoleLive.Show, :show
live "/admin/roles/:id/edit", RoleLive.Form, :edit
+ # Datafields (member fields + custom fields)
+ live "/admin/datafields", DatafieldsLive
+
# Import (Admin only)
live "/admin/import", ImportLive
@@ -112,7 +112,8 @@ defmodule MvWeb.Router do
auth_routes_prefix: "/auth",
on_mount: [{MvWeb.LiveUserAuth, :live_no_user}],
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
- gettext_backend: {MvWeb.Gettext, "auth"}
+ gettext_backend: {MvWeb.Gettext, "auth"},
+ live_view: MvWeb.SignInLive
# Remove this if you do not want to use the reset password feature
reset_route auth_routes_prefix: "/auth",
@@ -212,8 +213,8 @@ defmodule MvWeb.Router do
end)
end
- # Our supported languages for now are german and english, english as fallback language
+ # Our supported languages: German and English; default German.
defp supported_locale?(locale), do: locale in ["en", "de"]
- defp fallback_locale(nil), do: "en"
+ defp fallback_locale(nil), do: Application.get_env(:mv, :default_locale, "de")
defp fallback_locale(locale), do: locale
end
diff --git a/priv/gettext/auth.pot b/priv/gettext/auth.pot
index 8608e17..550b238 100644
--- a/priv/gettext/auth.pot
+++ b/priv/gettext/auth.pot
@@ -137,11 +137,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Language selection"
msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
+
+#: lib/mv_web/auth_overrides.ex
+#, elixir-autogen, elixir-format
+msgid "or"
+msgstr ""
diff --git a/priv/gettext/de/LC_MESSAGES/auth.po b/priv/gettext/de/LC_MESSAGES/auth.po
index 67e8551..4193b93 100644
--- a/priv/gettext/de/LC_MESSAGES/auth.po
+++ b/priv/gettext/de/LC_MESSAGES/auth.po
@@ -133,11 +133,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr "Dieses OIDC-Konto ist bereits mit einem anderen Benutzer verknüpft. Bitte kontaktiere den Support."
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Language selection"
msgstr "Sprachauswahl"
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr "Sprache auswählen"
+
+#: lib/mv_web/auth_overrides.ex
+#, elixir-autogen, elixir-format
+msgid "or"
+msgstr "oder"
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 2f4c1b8..49fbe83 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -18,6 +18,7 @@ msgid "Actions"
msgstr "Aktionen"
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
@@ -322,6 +323,7 @@ msgstr "Benutzer*innen auflisten"
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
@@ -335,6 +337,7 @@ msgstr "Mitglieder"
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex
@@ -382,7 +385,6 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member"
msgstr "Mitglied auswählen"
-#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Settings"
@@ -842,6 +844,7 @@ msgid "Create Member"
msgstr "Mitglied erstellen"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -853,11 +856,13 @@ msgstr "Betrag"
msgid "Back to Settings"
msgstr "Zurück zu den Einstellungen"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr "Kann jederzeit geändert werden. Änderungen des Betrags betreffen nur zukünftige Zyklen."
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Deletion"
@@ -868,6 +873,7 @@ msgstr "Löschen"
msgid "Examples"
msgstr "Beispiele"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
@@ -886,6 +892,7 @@ msgid "Half-yearly"
msgstr "Halbjährlich"
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -924,11 +931,13 @@ msgstr "Mitglied zahlt ab dem nächsten vollständigen Jahr"
msgid "Monthly"
msgstr "Monatlich"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr "Name & Betrag"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
@@ -1002,7 +1011,7 @@ msgstr "Alle auswählen"
msgid "Select none"
msgstr "Keine auswählen"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr "Eingegebener Text war nicht korrekt. Vorgang abgebrochen."
@@ -1044,11 +1053,6 @@ msgstr "Textfeld"
msgid "Yes/No-Selection"
msgstr "Ja/Nein-Auswahl"
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Memberdata"
-msgstr "Mitgliederdaten"
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format
@@ -1060,7 +1064,7 @@ msgstr "Optional"
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr "Diese Datenfelder sind für MILA notwendig um Mitglieder zu identifizieren und zukünftig Beitragszahlungen zu berechnen. Aus diesem Grund können sie nicht gelöscht, aber in der Übersicht ausgeblendet werden."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Member field %{action} successfully"
msgstr "Mitgliedsfeld wurde erfolgreich %{action}"
@@ -1070,6 +1074,7 @@ msgstr "Mitgliedsfeld wurde erfolgreich %{action}"
msgid "A cycle for this period already exists"
msgstr "Ein Zyklus für diesen Zeitraum existiert bereits"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "About Membership Fee Types"
@@ -1086,6 +1091,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr "Bereits bezahlte Zyklen bleiben mit dem alten Betrag."
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format
@@ -1097,6 +1103,7 @@ msgstr "Ein Fehler ist aufgetreten"
msgid "Are you sure you want to delete this cycle?"
msgstr "Möchtest du diesen Zyklus wirklich löschen?"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete - %{count} member(s) assigned"
@@ -1117,11 +1124,6 @@ msgstr "Die Änderung des Betrags betrifft %{count} Mitglied(er)."
msgid "Click to edit amount"
msgstr "Klicke, um den Betrag zu bearbeiten"
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Configure global settings for membership fees."
-msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
-
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
msgid "Confirm Change"
@@ -1232,6 +1234,7 @@ msgstr "Feld bearbeiten: %{field}"
msgid "Edit Membership Fee Type"
msgstr "Mitgliedsbeitragsart bearbeiten"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit membership fee type"
@@ -1330,6 +1333,7 @@ msgstr "Mitgliedsbeitragsstatus"
msgid "Membership Fee Type"
msgstr "Mitgliedsbeitragsart"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership Fee Types"
@@ -1346,6 +1350,7 @@ msgstr "Mitgliedsbeiträge"
msgid "Membership fee start"
msgstr "Beitragsbeginn"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership fee type deleted"
@@ -1366,6 +1371,7 @@ msgstr "Mitgliedsbeitragsart erfolgreich gespeichert"
msgid "Membership fee type updated. Cycles regenerated."
msgstr "Mitgliedsbeitragsart aktualisiert. Zyklen regeneriert."
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@@ -1376,6 +1382,7 @@ msgstr "Mitgliedsbeitragsarten definieren verschiedene Mitgliedsbeitragsstruktur
msgid "Monthly Interval - Joining Cycle Included"
msgstr "Monatliches Intervall – Beitrittszeitraum einbezogen"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -1550,6 +1557,7 @@ msgstr "Jährliches Intervall – Beitrittszeitraum einbezogen"
msgid "You are about to delete all %{count} cycles for this member."
msgstr "Du bist dabei alle %{count} Zyklen für dieses Mitglied zu löschen."
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Delete Membership Fee Type"
@@ -1571,12 +1579,12 @@ msgstr "Spalten ein-/ausblenden"
msgid "Back to settings"
msgstr "Zurück zu den Einstellungen"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Data field %{action} successfully"
msgstr "Datenfeld erfolgreich %{action}"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Data field deleted successfully"
msgstr "Datenfeld erfolgreich gelöscht"
@@ -1591,7 +1599,7 @@ msgstr "Datenfeld löschen"
msgid "Edit Data Field"
msgstr "Datenfeld bearbeiten"
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to delete data field: %{error}"
msgstr "Konnte Datenfeld nicht löschen: %{error}"
@@ -1823,6 +1831,7 @@ msgstr "Zyklus löschen"
msgid "The cycle period will be calculated based on this date and the interval."
msgstr "Der Zyklus wird basierend auf diesem Datum und dem Intervall berechnet."
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership fee type not found"
@@ -1843,6 +1852,7 @@ msgstr "Benutzer*in erfolgreich gelöscht"
msgid "User not found"
msgstr "Benutzer*in nicht gefunden"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this membership fee type"
@@ -1853,6 +1863,7 @@ msgstr "Du hast keine Berechtigung, auf diese Mitgliedsbeitragsart zuzugreifen"
msgid "You do not have permission to access this user"
msgstr "Du hast keine Berechtigung, auf diese*n Benutzer*in zuzugreifen"
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to delete this membership fee type"
@@ -1924,16 +1935,6 @@ msgstr "E-Mail ist erforderlich."
msgid "Roles"
msgstr "Rollen"
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Fee Settings"
-msgstr "Beitragseinstellungen"
-
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format
-msgid "Fee Types"
-msgstr "Beitragstypen"
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
@@ -2272,7 +2273,7 @@ msgstr "Dieser Benutzer kann nicht angezeigt werden."
msgid "Not authorized."
msgstr "Nicht berechtigt."
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions."
msgstr "Datenfelder konnten nicht geladen werden. Bitte überprüfe deine Berechtigungen."
@@ -2423,6 +2424,7 @@ msgstr "Beitragsart auswählen"
msgid "Linked"
msgstr "Verknüpft"
+#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -2682,6 +2684,61 @@ msgstr "Vereinfacht-Integration"
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr "Vereinfacht ist nicht konfiguriert. Bitte setze API-URL, API-Schlüssel und Vereins-ID."
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr "Integration testen"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr "Wird getestet..."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr "Verbindung erfolgreich. API-URL, API-Schlüssel und Vereins-ID sind korrekt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr "Nicht konfiguriert. Bitte API-URL, API-Schlüssel und Vereins-ID setzen."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr "Verbindung fehlgeschlagen (HTTP %{status}):"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr "Verbindung fehlgeschlagen (HTTP 401): API-Schlüssel ist ungültig oder fehlt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr "Verbindung fehlgeschlagen (HTTP 403): Zugriff verweigert. Bitte Vereins-ID und Berechtigungen des API-Schlüssels prüfen."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr "Verbindung fehlgeschlagen (HTTP 404): API-Endpunkt nicht gefunden. Bitte die API-URL prüfen (z. B. korrekter Versionspfad)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr "Verbindung fehlgeschlagen. Die URL zeigt nicht auf eine Vereinfacht-API (HTML statt JSON erhalten)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr "Verbindung fehlgeschlagen. API nicht erreichbar (Netzwerkfehler oder falsche URL)."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr "Verbindung fehlgeschlagen. Unbekannter Fehler."
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"
@@ -2920,3 +2977,124 @@ msgstr "Für die Vereinfacht-Integration erforderlich und kann nicht deaktiviert
#, elixir-autogen, elixir-format
msgid "Fee Type"
msgstr "Beitragsart"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Admin group name"
+msgstr "Admin-Gruppenname"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Base URL"
+msgstr "Basis-URL"
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format
+msgid "Basic settings"
+msgstr "Grundeinstellungen"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client ID"
+msgstr "Client-ID"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client Secret"
+msgstr "Client-Geheimnis"
+
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Configure global settings and fee types for membership fees."
+msgstr "Globale Einstellungen für Mitgliedsbeiträge konfigurieren."
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure member fields and custom data fields."
+msgstr "Mitgliedsfelder und benutzerdefinierte Datenfelder konfigurieren."
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Custom fields"
+msgstr "Benutzerdefinierte Felder"
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Datafields"
+msgstr "Datenfelder"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_ADMIN_GROUP_NAME"
+msgstr "Aus OIDC_ADMIN_GROUP_NAME"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_BASE_URL"
+msgstr "Aus OIDC_BASE_URL"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_ID"
+msgstr "Aus OIDC_CLIENT_ID"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_SECRET"
+msgstr "Aus OIDC_CLIENT_SECRET"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_GROUPS_CLAIM"
+msgstr "Aus OIDC_GROUPS_CLAIM"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_REDIRECT_URI"
+msgstr "Aus OIDC_REDIRECT_URI"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Groups claim"
+msgstr "Gruppenclaim"
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Member fields"
+msgstr "Mitgliedsfelder"
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Membership fee settings"
+msgstr "Beitragseinstellungen"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Redirect URI"
+msgstr "Weiterleitungs-URI"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Save OIDC Settings"
+msgstr "OIDC-Einstellungen speichern"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "e.g. admin"
+msgstr "z. B. admin"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_ONLY"
+msgstr "Aus OIDC_ONLY"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Only OIDC sign-in (hide password login)"
+msgstr "Nur OIDC-Anmeldung (Passwort-Login ausblenden)"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
+msgstr "Wenn aktiviert und OIDC konfiguriert ist, zeigt die Anmeldeseite nur den Single-Sign-On-Button."
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 98c2b91..ea8e976 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -19,6 +19,7 @@ msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
@@ -323,6 +324,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
@@ -336,6 +338,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex
@@ -383,7 +386,6 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Settings"
@@ -843,6 +845,7 @@ msgid "Create Member"
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -854,11 +857,13 @@ msgstr ""
msgid "Back to Settings"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Deletion"
@@ -869,6 +874,7 @@ msgstr ""
msgid "Examples"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
@@ -887,6 +893,7 @@ msgid "Half-yearly"
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -925,11 +932,13 @@ msgstr ""
msgid "Monthly"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
@@ -1003,7 +1012,7 @@ msgstr ""
msgid "Select none"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr ""
@@ -1045,11 +1054,6 @@ msgstr ""
msgid "Yes/No-Selection"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Memberdata"
-msgstr ""
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format
@@ -1061,7 +1065,7 @@ msgstr ""
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Member field %{action} successfully"
msgstr ""
@@ -1071,6 +1075,7 @@ msgstr ""
msgid "A cycle for this period already exists"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "About Membership Fee Types"
@@ -1087,6 +1092,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format
@@ -1098,6 +1104,7 @@ msgstr ""
msgid "Are you sure you want to delete this cycle?"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Cannot delete - %{count} member(s) assigned"
@@ -1118,11 +1125,6 @@ msgstr ""
msgid "Click to edit amount"
msgstr ""
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format
-msgid "Configure global settings for membership fees."
-msgstr ""
-
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
msgid "Confirm Change"
@@ -1233,6 +1235,7 @@ msgstr ""
msgid "Edit Membership Fee Type"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Edit membership fee type"
@@ -1331,6 +1334,7 @@ msgstr ""
msgid "Membership Fee Type"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership Fee Types"
@@ -1347,6 +1351,7 @@ msgstr ""
msgid "Membership fee start"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership fee type deleted"
@@ -1367,6 +1372,7 @@ msgstr ""
msgid "Membership fee type updated. Cycles regenerated."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@@ -1377,6 +1383,7 @@ msgstr ""
msgid "Monthly Interval - Joining Cycle Included"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -1551,6 +1558,7 @@ msgstr ""
msgid "You are about to delete all %{count} cycles for this member."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Delete Membership Fee Type"
@@ -1572,12 +1580,12 @@ msgstr ""
msgid "Back to settings"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Data field %{action} successfully"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Data field deleted successfully"
msgstr ""
@@ -1592,7 +1600,7 @@ msgstr ""
msgid "Edit Data Field"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Failed to delete data field: %{error}"
msgstr ""
@@ -1824,6 +1832,7 @@ msgstr ""
msgid "The cycle period will be calculated based on this date and the interval."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Membership fee type not found"
@@ -1844,6 +1853,7 @@ msgstr ""
msgid "User not found"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to access this membership fee type"
@@ -1854,6 +1864,7 @@ msgstr ""
msgid "You do not have permission to access this user"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "You do not have permission to delete this membership fee type"
@@ -1925,16 +1936,6 @@ msgstr ""
msgid "Roles"
msgstr ""
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format
-msgid "Fee Settings"
-msgstr ""
-
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format
-msgid "Fee Types"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
@@ -2273,7 +2274,7 @@ msgstr ""
msgid "Not authorized."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions."
msgstr ""
@@ -2424,6 +2425,7 @@ msgstr ""
msgid "Linked"
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -2683,6 +2685,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr ""
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"
@@ -2920,3 +2977,124 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "Fee Type"
msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Admin group name"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Base URL"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format
+msgid "Basic settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client ID"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client Secret"
+msgstr ""
+
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure global settings and fee types for membership fees."
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure member fields and custom data fields."
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Custom fields"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Datafields"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_ADMIN_GROUP_NAME"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_BASE_URL"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_ID"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_SECRET"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_GROUPS_CLAIM"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_REDIRECT_URI"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Groups claim"
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Member fields"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format
+msgid "Membership fee settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Redirect URI"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Save OIDC Settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "e.g. admin"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_ONLY"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Only OIDC sign-in (hide password login)"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/auth.po b/priv/gettext/en/LC_MESSAGES/auth.po
index 0ec49bc..d3511b7 100644
--- a/priv/gettext/en/LC_MESSAGES/auth.po
+++ b/priv/gettext/en/LC_MESSAGES/auth.po
@@ -130,11 +130,18 @@ msgid "This OIDC account is already linked to another user. Please contact suppo
msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Language selection"
msgstr ""
#: lib/mv_web/live/auth/link_oidc_account_live.ex
+#: lib/mv_web/live/auth/sign_in_live.ex
#, elixir-autogen, elixir-format
msgid "Select language"
msgstr ""
+
+#: lib/mv_web/auth_overrides.ex
+#, elixir-autogen, elixir-format
+msgid "or"
+msgstr "or"
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index a76c9f6..915fc52 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -19,6 +19,7 @@ msgid "Actions"
msgstr ""
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/index.html.heex
#: lib/mv_web/live/role_live/show.ex
@@ -323,6 +324,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/statistics_live.ex
#, elixir-autogen, elixir-format
@@ -336,6 +338,7 @@ msgstr ""
#: lib/mv_web/live/group_live/show.ex
#: lib/mv_web/live/member_field_live/form_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/form.ex
@@ -383,7 +386,6 @@ msgstr ""
msgid "Select member"
msgstr ""
-#: lib/mv_web/components/layouts/sidebar.ex
#: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format
msgid "Settings"
@@ -843,6 +845,7 @@ msgid "Create Member"
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -854,11 +857,13 @@ msgstr ""
msgid "Back to Settings"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Can be changed at any time. Amount changes affect future periods only."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Deletion"
@@ -869,6 +874,7 @@ msgstr ""
msgid "Examples"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Fixed after creation. Members can only switch between types with the same interval."
@@ -887,6 +893,7 @@ msgid "Half-yearly"
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
@@ -925,11 +932,13 @@ msgstr ""
msgid "Monthly"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Name & Amount"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "Only possible if no members are assigned to this type."
@@ -1003,7 +1012,7 @@ msgstr ""
msgid "Select none"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Slug does not match. Deletion cancelled."
msgstr ""
@@ -1045,11 +1054,6 @@ msgstr ""
msgid "Yes/No-Selection"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Memberdata"
-msgstr ""
-
#: lib/mv_web/live/custom_field_live/index_component.ex
#: lib/mv_web/live/member_field_live/index_component.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1061,7 +1065,7 @@ msgstr ""
msgid "These fields are neccessary for MILA to handle member identification and payment calculations in the future. Thus you cannot delete these fields but hide them in the member overview."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Member field %{action} successfully"
msgstr ""
@@ -1071,6 +1075,7 @@ msgstr ""
msgid "A cycle for this period already exists"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format
msgid "About Membership Fee Types"
@@ -1087,6 +1092,7 @@ msgid "Already paid cycles will remain with the old amount."
msgstr ""
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#: lib/mv_web/live/role_live/helpers.ex
#, elixir-autogen, elixir-format
@@ -1098,6 +1104,7 @@ msgstr ""
msgid "Are you sure you want to delete this cycle?"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Cannot delete - %{count} member(s) assigned"
@@ -1118,11 +1125,6 @@ msgstr ""
msgid "Click to edit amount"
msgstr ""
-#: lib/mv_web/live/membership_fee_settings_live.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Configure global settings for membership fees."
-msgstr ""
-
#: lib/mv_web/live/membership_fee_type_live/form.ex
#, elixir-autogen, elixir-format
msgid "Confirm Change"
@@ -1233,6 +1235,7 @@ msgstr ""
msgid "Edit Membership Fee Type"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Edit membership fee type"
@@ -1331,6 +1334,7 @@ msgstr ""
msgid "Membership Fee Type"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership Fee Types"
@@ -1347,6 +1351,7 @@ msgstr ""
msgid "Membership fee start"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee type deleted"
@@ -1367,6 +1372,7 @@ msgstr ""
msgid "Membership fee type updated. Cycles regenerated."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
@@ -1377,6 +1383,7 @@ msgstr ""
msgid "Monthly Interval - Joining Cycle Included"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/form.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
@@ -1551,6 +1558,7 @@ msgstr ""
msgid "You are about to delete all %{count} cycles for this member."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Delete Membership Fee Type"
@@ -1572,12 +1580,12 @@ msgstr ""
msgid "Back to settings"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Data field %{action} successfully"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Data field deleted successfully"
msgstr ""
@@ -1592,7 +1600,7 @@ msgstr ""
msgid "Edit Data Field"
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Failed to delete data field: %{error}"
msgstr ""
@@ -1824,6 +1832,7 @@ msgstr ""
msgid "The cycle period will be calculated based on this date and the interval."
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "Membership fee type not found"
@@ -1844,6 +1853,7 @@ msgstr ""
msgid "User not found"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to access this membership fee type"
@@ -1854,6 +1864,7 @@ msgstr ""
msgid "You do not have permission to access this user"
msgstr ""
+#: lib/mv_web/live/membership_fee_settings_live.ex
#: lib/mv_web/live/membership_fee_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy
msgid "You do not have permission to delete this membership fee type"
@@ -1925,16 +1936,6 @@ msgstr ""
msgid "Roles"
msgstr ""
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format, fuzzy
-msgid "Fee Settings"
-msgstr ""
-
-#: lib/mv_web/components/layouts/sidebar.ex
-#, elixir-autogen, elixir-format
-msgid "Fee Types"
-msgstr ""
-
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Administration"
@@ -2273,7 +2274,7 @@ msgstr ""
msgid "Not authorized."
msgstr ""
-#: lib/mv_web/live/global_settings_live.ex
+#: lib/mv_web/live/datafields_live.ex
#, elixir-autogen, elixir-format
msgid "Could not load data fields. Please check your permissions."
msgstr ""
@@ -2424,6 +2425,7 @@ msgstr ""
msgid "Linked"
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
#: lib/mv_web/live/user_live/index.html.heex
#: lib/mv_web/live/user_live/show.ex
#, elixir-autogen, elixir-format
@@ -2683,6 +2685,61 @@ msgstr ""
msgid "Vereinfacht is not configured. Set API URL, API Key, and Club ID."
msgstr ""
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Test Integration"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Testing..."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection successful. API URL, API Key and Club ID are valid."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Not configured. Please set API URL, API Key and Club ID."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP %{status}):"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 401): API key is invalid or missing."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 403): Access denied. Please check the Club ID and API key permissions."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed (HTTP 404): API endpoint not found. Please check the API URL (e.g. correct version path)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. The URL does not point to a Vereinfacht API (received HTML instead of JSON)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Could not reach the API (network error or wrong URL)."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Connection failed. Unknown error."
+msgstr ""
+
#: lib/mv_web/live/member_live/show/membership_fees_component.ex
#, elixir-autogen, elixir-format
msgid "View contact in Vereinfacht"
@@ -2920,3 +2977,124 @@ msgstr "Required for Vereinfacht integration and cannot be disabled."
#, elixir-autogen, elixir-format
msgid "Fee Type"
msgstr "Fee Type"
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Admin group name"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Base URL"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format
+msgid "Basic settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client ID"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Client Secret"
+msgstr ""
+
+#: lib/mv_web/live/membership_fee_settings_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Configure global settings and fee types for membership fees."
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Configure member fields and custom data fields."
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Custom fields"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format
+msgid "Datafields"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_ADMIN_GROUP_NAME"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_BASE_URL"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_ID"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_CLIENT_SECRET"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_GROUPS_CLAIM"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From OIDC_REDIRECT_URI"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Groups claim"
+msgstr ""
+
+#: lib/mv_web/live/datafields_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Member fields"
+msgstr ""
+
+#: lib/mv_web/components/layouts/sidebar.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Membership fee settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Redirect URI"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Save OIDC Settings"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "e.g. admin"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format, fuzzy
+msgid "From OIDC_ONLY"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Only OIDC sign-in (hide password login)"
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "When enabled and OIDC is configured, the sign-in page shows only the Single Sign-On button."
+msgstr ""
diff --git a/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs b/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs
new file mode 100644
index 0000000..154709a
--- /dev/null
+++ b/priv/repo/migrations/20260224122831_add_oidc_to_settings.exs
@@ -0,0 +1,29 @@
+defmodule Mv.Repo.Migrations.AddOidcToSettings do
+ @moduledoc """
+ Adds OIDC configuration columns to settings (ENV-overridable in UI).
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:settings) do
+ add :oidc_client_id, :string
+ add :oidc_base_url, :string
+ add :oidc_redirect_uri, :string
+ add :oidc_client_secret, :string
+ add :oidc_admin_group_name, :string
+ add :oidc_groups_claim, :string
+ end
+ end
+
+ def down do
+ alter table(:settings) do
+ remove :oidc_client_id
+ remove :oidc_base_url
+ remove :oidc_redirect_uri
+ remove :oidc_client_secret
+ remove :oidc_admin_group_name
+ remove :oidc_groups_claim
+ end
+ end
+end
diff --git a/priv/repo/migrations/20260224180000_add_oidc_only_to_settings.exs b/priv/repo/migrations/20260224180000_add_oidc_only_to_settings.exs
new file mode 100644
index 0000000..3775ef6
--- /dev/null
+++ b/priv/repo/migrations/20260224180000_add_oidc_only_to_settings.exs
@@ -0,0 +1,20 @@
+defmodule Mv.Repo.Migrations.AddOidcOnlyToSettings do
+ @moduledoc """
+ Adds oidc_only flag to settings. When true and OIDC is configured,
+ the sign-in page shows only OIDC (password login is hidden).
+ """
+
+ use Ecto.Migration
+
+ def up do
+ alter table(:settings) do
+ add :oidc_only, :boolean, default: false, null: false
+ end
+ end
+
+ def down do
+ alter table(:settings) do
+ remove :oidc_only
+ end
+ end
+end
diff --git a/priv/resource_snapshots/repo/settings/20251127134451.json b/priv/resource_snapshots/repo/settings/20251127134451.json
index fefc223..8767574 100644
--- a/priv/resource_snapshots/repo/settings/20251127134451.json
+++ b/priv/resource_snapshots/repo/settings/20251127134451.json
@@ -64,4 +64,4 @@
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
-}
\ No newline at end of file
+}
diff --git a/priv/resource_snapshots/repo/settings/20251201115939.json b/priv/resource_snapshots/repo/settings/20251201115939.json
index 4e635c4..da284c6 100644
--- a/priv/resource_snapshots/repo/settings/20251201115939.json
+++ b/priv/resource_snapshots/repo/settings/20251201115939.json
@@ -76,4 +76,4 @@
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
-}
\ No newline at end of file
+}
diff --git a/priv/resource_snapshots/repo/settings/20251211195058.json b/priv/resource_snapshots/repo/settings/20251211195058.json
index 4b437b8..ea73ec3 100644
--- a/priv/resource_snapshots/repo/settings/20251211195058.json
+++ b/priv/resource_snapshots/repo/settings/20251211195058.json
@@ -100,4 +100,4 @@
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
-}
\ No newline at end of file
+}
diff --git a/priv/resource_snapshots/repo/settings/20260218185541.json b/priv/resource_snapshots/repo/settings/20260218185541.json
index 4334f9a..9c7ef33 100644
--- a/priv/resource_snapshots/repo/settings/20260218185541.json
+++ b/priv/resource_snapshots/repo/settings/20260218185541.json
@@ -137,4 +137,4 @@
"repo": "Elixir.Mv.Repo",
"schema": null,
"table": "settings"
-}
\ No newline at end of file
+}
diff --git a/priv/resource_snapshots/repo/settings/20260224122831.json b/priv/resource_snapshots/repo/settings/20260224122831.json
new file mode 100644
index 0000000..73678b4
--- /dev/null
+++ b/priv/resource_snapshots/repo/settings/20260224122831.json
@@ -0,0 +1,164 @@
+{
+ "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": "club_name",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "member_field_visibility",
+ "type": "map"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "member_field_required",
+ "type": "map"
+ },
+ {
+ "allow_nil?": false,
+ "default": "true",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "include_joining_cycle",
+ "type": "boolean"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "default_membership_fee_type_id",
+ "type": "uuid"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "vereinfacht_api_url",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "vereinfacht_api_key",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "vereinfacht_club_id",
+ "type": "text"
+ },
+ {
+ "allow_nil?": true,
+ "default": "nil",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "vereinfacht_app_url",
+ "type": "text"
+ },
+ {
+ "allow_nil?": false,
+ "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
+ "generated?": false,
+ "precision": null,
+ "primary_key?": false,
+ "references": null,
+ "scale": null,
+ "size": null,
+ "source": "inserted_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": "updated_at",
+ "type": "utc_datetime_usec"
+ }
+ ],
+ "base_filter": null,
+ "check_constraints": [],
+ "create_table_options": null,
+ "custom_indexes": [],
+ "custom_statements": [],
+ "has_create_action": true,
+ "hash": "C84FC81A2A446451D6B5EA72F9BBB3593CD7F0D71C4B7C9CE04934414FDB52EB",
+ "identities": [],
+ "multitenancy": {
+ "attribute": null,
+ "global": null,
+ "strategy": null
+ },
+ "repo": "Elixir.Mv.Repo",
+ "schema": null,
+ "table": "settings"
+}
diff --git a/test/mv/oidc_role_sync_config_test.exs b/test/mv/oidc_role_sync_config_test.exs
index b4664aa..4b77378 100644
--- a/test/mv/oidc_role_sync_config_test.exs
+++ b/test/mv/oidc_role_sync_config_test.exs
@@ -1,21 +1,22 @@
defmodule Mv.OidcRoleSyncConfigTest do
@moduledoc """
Tests for OIDC role sync configuration (OIDC_ADMIN_GROUP_NAME, OIDC_GROUPS_CLAIM).
+ Reads via Mv.Config (ENV first, then Settings).
"""
- use ExUnit.Case, async: false
+ use Mv.DataCase, async: false
alias Mv.OidcRoleSyncConfig
describe "oidc_admin_group_name/0" do
test "returns nil when OIDC_ADMIN_GROUP_NAME is not configured" do
- restore = put_config(admin_group_name: nil)
+ restore = clear_env("OIDC_ADMIN_GROUP_NAME")
on_exit(restore)
assert OidcRoleSyncConfig.oidc_admin_group_name() == nil
end
- test "returns configured admin group name when set" do
- restore = put_config(admin_group_name: "mila-admin")
+ test "returns configured admin group name when set via ENV" do
+ restore = set_env("OIDC_ADMIN_GROUP_NAME", "mila-admin")
on_exit(restore)
assert OidcRoleSyncConfig.oidc_admin_group_name() == "mila-admin"
@@ -24,26 +25,35 @@ defmodule Mv.OidcRoleSyncConfigTest do
describe "oidc_groups_claim/0" do
test "returns default \"groups\" when OIDC_GROUPS_CLAIM is not configured" do
- restore = put_config(groups_claim: nil)
+ restore = clear_env("OIDC_GROUPS_CLAIM")
on_exit(restore)
assert OidcRoleSyncConfig.oidc_groups_claim() == "groups"
end
- test "returns configured claim name when OIDC_GROUPS_CLAIM is set" do
- restore = put_config(groups_claim: "ak_groups")
+ test "returns configured claim name when OIDC_GROUPS_CLAIM is set via ENV" do
+ restore = set_env("OIDC_GROUPS_CLAIM", "ak_groups")
on_exit(restore)
assert OidcRoleSyncConfig.oidc_groups_claim() == "ak_groups"
end
end
- defp put_config(opts) do
- current = Application.get_env(:mv, :oidc_role_sync, [])
- Application.put_env(:mv, :oidc_role_sync, Keyword.merge(current, opts))
+ defp set_env(key, value) do
+ previous = System.get_env(key)
+ System.put_env(key, value)
fn ->
- Application.put_env(:mv, :oidc_role_sync, current)
+ if previous, do: System.put_env(key, previous), else: System.delete_env(key)
+ end
+ end
+
+ defp clear_env(key) do
+ previous = System.get_env(key)
+ System.delete_env(key)
+
+ fn ->
+ if previous, do: System.put_env(key, previous)
end
end
end
diff --git a/test/mv/oidc_role_sync_test.exs b/test/mv/oidc_role_sync_test.exs
index 229f56a..a4729ec 100644
--- a/test/mv/oidc_role_sync_test.exs
+++ b/test/mv/oidc_role_sync_test.exs
@@ -12,14 +12,14 @@ defmodule Mv.OidcRoleSyncTest do
setup do
ensure_roles_exist()
- restore_config = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "groups")
+ restore_config = put_oidc_env(admin_group_name: "mila-admin", groups_claim: "groups")
on_exit(restore_config)
:ok
end
describe "apply_admin_role_from_user_info/2" do
test "when OIDC_ADMIN_GROUP_NAME not configured: does not change user (Mitglied stays)" do
- restore = put_oidc_config(admin_group_name: nil, groups_claim: "groups")
+ restore = put_oidc_env(admin_group_name: nil, groups_claim: "groups")
on_exit(restore)
email = "sync-no-config-#{System.unique_integer([:positive])}@test.example.com"
@@ -58,7 +58,7 @@ defmodule Mv.OidcRoleSyncTest do
end
test "when OIDC_GROUPS_CLAIM is different: reads groups from that claim" do
- restore = put_oidc_config(admin_group_name: "mila-admin", groups_claim: "ak_groups")
+ restore = put_oidc_env(admin_group_name: "mila-admin", groups_claim: "ak_groups")
on_exit(restore)
email = "sync-claim-#{System.unique_integer([:positive])}@test.example.com"
@@ -131,13 +131,30 @@ defmodule Mv.OidcRoleSyncTest do
end
end
- defp put_oidc_config(opts) do
- current = Application.get_env(:mv, :oidc_role_sync, [])
- merged = Keyword.merge(current, opts)
- Application.put_env(:mv, :oidc_role_sync, merged)
+ defp put_oidc_env(opts) do
+ prev_admin = System.get_env("OIDC_ADMIN_GROUP_NAME")
+ prev_claim = System.get_env("OIDC_GROUPS_CLAIM")
+
+ if opts[:admin_group_name] != nil do
+ System.put_env("OIDC_ADMIN_GROUP_NAME", to_string(opts[:admin_group_name]))
+ else
+ System.delete_env("OIDC_ADMIN_GROUP_NAME")
+ end
+
+ if opts[:groups_claim] != nil do
+ System.put_env("OIDC_GROUPS_CLAIM", to_string(opts[:groups_claim]))
+ else
+ System.delete_env("OIDC_GROUPS_CLAIM")
+ end
fn ->
- Application.put_env(:mv, :oidc_role_sync, current)
+ if prev_admin,
+ do: System.put_env("OIDC_ADMIN_GROUP_NAME", prev_admin),
+ else: System.delete_env("OIDC_ADMIN_GROUP_NAME")
+
+ if prev_claim,
+ do: System.put_env("OIDC_GROUPS_CLAIM", prev_claim),
+ else: System.delete_env("OIDC_GROUPS_CLAIM")
end
end
diff --git a/test/mv_web/components/sidebar_authorization_test.exs b/test/mv_web/components/sidebar_authorization_test.exs
index 110d9e5..5d4277b 100644
--- a/test/mv_web/components/sidebar_authorization_test.exs
+++ b/test/mv_web/components/sidebar_authorization_test.exs
@@ -30,7 +30,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user))
assert html =~ ~s(href="/members")
- assert html =~ ~s(href="/membership_fee_types")
+ assert html =~ ~s(href="/membership_fee_settings")
assert html =~ ~s(href="/statistics")
assert html =~ ~s(data-testid="sidebar-administration")
assert html =~ ~s(href="/users")
@@ -55,7 +55,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
user = Fixtures.user_with_role_fixture("read_only")
html = render_sidebar(sidebar_assigns(user))
- refute html =~ ~s(href="/membership_fee_types")
+ refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings")
@@ -76,7 +76,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
user = Fixtures.user_with_role_fixture("normal_user")
html = render_sidebar(sidebar_assigns(user))
- refute html =~ ~s(href="/membership_fee_types")
+ refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users")
refute html =~ ~s(href="/admin/roles")
refute html =~ ~s(href="/settings")
@@ -96,7 +96,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/statistics")
- refute html =~ ~s(href="/membership_fee_types")
+ refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users")
refute html =~ ~s(data-testid="sidebar-administration")
end
@@ -117,7 +117,7 @@ defmodule MvWeb.SidebarAuthorizationTest do
html = render_sidebar(sidebar_assigns(user))
refute html =~ ~s(href="/members")
- refute html =~ ~s(href="/membership_fee_types")
+ refute html =~ ~s(href="/membership_fee_settings")
refute html =~ ~s(href="/users")
end
end
diff --git a/test/mv_web/live/custom_field_live/deletion_test.exs b/test/mv_web/live/custom_field_live/deletion_test.exs
index 36e46e2..28f98a2 100644
--- a/test/mv_web/live/custom_field_live/deletion_test.exs
+++ b/test/mv_web/live/custom_field_live/deletion_test.exs
@@ -54,7 +54,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
# Create custom field value
create_custom_field_value(member, custom_field, "test")
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Click delete button - find the delete link within the component
view
@@ -80,7 +80,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
create_custom_field_value(member1, custom_field, "test1")
create_custom_field_value(member2, custom_field, "test2")
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -93,7 +93,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "shows 0 members for custom field without values", %{conn: conn} do
{:ok, _custom_field} = create_custom_field("test_field", :string)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -108,7 +108,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "updates confirmation state when typing", %{conn: conn} do
{:ok, custom_field} = create_custom_field("test_field", :string)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -126,7 +126,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "delete button is disabled when slug doesn't match", %{conn: conn} do
{:ok, _custom_field} = create_custom_field("test_field", :string)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -148,7 +148,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
{:ok, custom_field} = create_custom_field("test_field", :string)
{:ok, custom_field_value} = create_custom_field_value(member, custom_field, "test")
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Open modal
view
@@ -185,7 +185,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
} do
{:ok, custom_field} = create_custom_field("test_field", :string)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -209,7 +209,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
test "closes modal without deleting", %{conn: conn} do
{:ok, custom_field} = create_custom_field("test_field", :string)
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
view
|> element("#custom-fields-component a", "Delete")
@@ -234,7 +234,7 @@ defmodule MvWeb.CustomFieldLive.DeletionTest do
describe "create custom field" do
test "submitting new data field form creates custom field and shows success", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
+ {:ok, view, _html} = live(conn, ~p"/admin/datafields")
# Open "New Data Field" form
view
diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs
index 86680f3..6a739b5 100644
--- a/test/mv_web/live/global_settings_live_test.exs
+++ b/test/mv_web/live/global_settings_live_test.exs
@@ -64,21 +64,5 @@ defmodule MvWeb.GlobalSettingsLiveTest do
assert html =~ "must be present"
end
-
- test "displays Memberdata section", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
-
- assert html =~ "Memberdata" or html =~ "Member Data"
- end
-
- test "displays flash message after member field visibility update", %{conn: conn} do
- {:ok, view, _html} = live(conn, ~p"/settings")
-
- # Simulate member field visibility update
- send(view.pid, {:member_field_visibility_updated})
-
- # Check for flash message
- assert render(view) =~ "updated" or render(view) =~ "success"
- end
end
end
diff --git a/test/mv_web/live/member_field_live/index_component_test.exs b/test/mv_web/live/member_field_live/index_component_test.exs
index 6ad1627..d3c1612 100644
--- a/test/mv_web/live/member_field_live/index_component_test.exs
+++ b/test/mv_web/live/member_field_live/index_component_test.exs
@@ -23,7 +23,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
describe "rendering" do
test "renders all member fields from Constants", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Check that all member fields are displayed
member_fields = Mv.Constants.member_fields()
@@ -36,7 +36,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end
test "displays show_in_overview status as badge", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Should have "Show in overview" column header
assert html =~ "Show in overview" or html =~ "Show in Overview"
@@ -46,7 +46,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end
test "displays required status column", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Should have "Required" column; email is always required
assert html =~ "Required" or html =~ "required"
@@ -59,7 +59,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
{:ok, _updated} =
Membership.update_settings(settings, %{member_field_visibility: %{}})
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# All fields should show as visible (Yes) by default
# Check for "Yes" badge or similar indicator
@@ -74,7 +74,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
{:ok, _updated} =
Membership.update_member_field_visibility(settings, visibility_config)
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Street and house_number should show as hidden (No)
# Other fields should show as visible (Yes)
@@ -102,7 +102,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end
test "marks email as required (always from settings)", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Email is always required
assert html =~ "email" or html =~ "Email"
@@ -119,7 +119,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
required: true
)
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# First name row should show Required (and Optional for others)
assert html =~ "First name" or html =~ "first_name"
@@ -127,7 +127,7 @@ defmodule MvWeb.MemberFieldLive.IndexComponentTest do
end
test "optional fields show Optional when not required in settings", %{conn: conn} do
- {:ok, _view, html} = live(conn, ~p"/settings")
+ {:ok, _view, html} = live(conn, ~p"/admin/datafields")
# Email is required; other fields default to optional
assert html =~ "Optional"
diff --git a/test/mv_web/live/membership_fee_type_live/form_test.exs b/test/mv_web/live/membership_fee_type_live/form_test.exs
index 71edbba..a836c4d 100644
--- a/test/mv_web/live/membership_fee_type_live/form_test.exs
+++ b/test/mv_web/live/membership_fee_type_live/form_test.exs
@@ -11,7 +11,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
require Ash.Query
setup %{conn: conn} do
- # User must have admin role (or normal_user) to access /membership_fee_types pages
+ # User must have admin role (or normal_user) to access /membership_fee_settings pages
user = Mv.Fixtures.user_with_role_fixture("admin")
authenticated_conn = conn_with_password_user(conn, user)
%{conn: authenticated_conn, user: user}
@@ -51,7 +51,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
describe "create form" do
test "creates new membership fee type", %{conn: conn, user: user} do
- {:ok, view, _html} = live(conn, "/membership_fee_types/new")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/new_fee_type")
form_data = %{
"membership_fee_type[name]" => "New Type",
@@ -65,7 +65,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
|> form("#membership-fee-type-form", form_data)
|> render_submit()
- assert to == "/membership_fee_types"
+ assert to == "/membership_fee_settings"
# Verify type was created (use actor so read is authorized)
type =
@@ -79,7 +79,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
end
test "interval field is editable on create", %{conn: conn} do
- {:ok, _view, html} = live(conn, "/membership_fee_types/new")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings/new_fee_type")
# Interval field should be editable (not disabled)
refute html =~ "disabled" || html =~ "readonly"
@@ -90,7 +90,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "loads existing type data", %{conn: conn} do
fee_type = create_fee_type(%{name: "Existing Type", amount: Decimal.new("60.00")})
- {:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
assert html =~ "Existing Type"
assert html =~ "60" || html =~ "60,00"
@@ -99,7 +99,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "interval field is grayed out on edit", %{conn: conn} do
fee_type = create_fee_type(%{interval: :yearly})
- {:ok, _view, html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Interval field should be disabled
assert html =~ "disabled" || html =~ "readonly"
@@ -109,7 +109,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
create_member(%{membership_fee_type_id: fee_type.id})
- {:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount
view
@@ -129,7 +129,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
create_member(%{membership_fee_type_id: fee_type.id})
end)
- {:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount
html =
@@ -144,7 +144,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "amount change can be confirmed", %{conn: conn, user: user} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
- {:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount and confirm
view
@@ -173,7 +173,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
test "amount change can be cancelled", %{conn: conn, user: user} do
fee_type = create_fee_type(%{amount: Decimal.new("50.00")})
- {:ok, view, _html} = live(conn, "/membership_fee_types/#{fee_type.id}/edit")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/#{fee_type.id}/edit_fee_type")
# Change amount and cancel
view
@@ -195,7 +195,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
end
test "validation errors display correctly", %{conn: conn} do
- {:ok, view, _html} = live(conn, "/membership_fee_types/new")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings/new_fee_type")
# Submit with invalid data
html =
@@ -214,7 +214,7 @@ defmodule MvWeb.MembershipFeeTypeLive.FormTest do
describe "permissions" do
test "only admin can access", %{conn: conn} do
# This test assumes non-admin users cannot access
- {:ok, _view, html} = live(conn, "/membership_fee_types/new")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings/new_fee_type")
# Should show the form (admin user in setup)
assert html =~ "Membership Fee Type" || html =~ "Beitragsart"
diff --git a/test/mv_web/live/membership_fee_type_live/index_test.exs b/test/mv_web/live/membership_fee_type_live/index_test.exs
index 7d2d4be..c9bb7ca 100644
--- a/test/mv_web/live/membership_fee_type_live/index_test.exs
+++ b/test/mv_web/live/membership_fee_type_live/index_test.exs
@@ -60,7 +60,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
admin_user
)
- {:ok, _view, html} = live(conn, "/membership_fee_types")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings")
assert html =~ "Regular"
assert html =~ "Reduced"
@@ -77,33 +77,33 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
create_member(%{membership_fee_type_id: fee_type.id}, admin_user)
end)
- {:ok, _view, html} = live(conn, "/membership_fee_types")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings")
assert html =~ "3" || html =~ "Members" || html =~ "Mitglieder"
end
test "create button navigates to form", %{conn: conn} do
- {:ok, view, _html} = live(conn, "/membership_fee_types")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings")
{:error, {:live_redirect, %{to: to}}} =
view
- |> element("a[href='/membership_fee_types/new']")
+ |> element("a[href='/membership_fee_settings/new_fee_type']")
|> render_click()
- assert to == "/membership_fee_types/new"
+ assert to == "/membership_fee_settings/new_fee_type"
end
test "edit button per row navigates to edit form", %{conn: conn, current_user: admin_user} do
fee_type = create_fee_type(%{interval: :yearly}, admin_user)
- {:ok, view, _html} = live(conn, "/membership_fee_types")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings")
{:error, {:live_redirect, %{to: to}}} =
view
- |> element("a[href='/membership_fee_types/#{fee_type.id}/edit']")
+ |> element("a[href='/membership_fee_settings/#{fee_type.id}/edit_fee_type']")
|> render_click()
- assert to == "/membership_fee_types/#{fee_type.id}/edit"
+ assert to == "/membership_fee_settings/#{fee_type.id}/edit_fee_type"
end
end
@@ -112,7 +112,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
fee_type = create_fee_type(%{interval: :yearly}, admin_user)
create_member(%{membership_fee_type_id: fee_type.id}, admin_user)
- {:ok, _view, html} = live(conn, "/membership_fee_types")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings")
# Delete button should be disabled
assert html =~ "disabled" || html =~ "cursor-not-allowed"
@@ -122,7 +122,7 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
fee_type = create_fee_type(%{interval: :yearly}, admin_user)
# No members assigned
- {:ok, view, _html} = live(conn, "/membership_fee_types")
+ {:ok, view, _html} = live(conn, "/membership_fee_settings")
# Delete button should be enabled
view
@@ -142,10 +142,11 @@ defmodule MvWeb.MembershipFeeTypeLive.IndexTest do
test "only admin can access", %{conn: conn} do
# This test assumes non-admin users cannot access
# Adjust based on actual permission implementation
- {:ok, _view, html} = live(conn, "/membership_fee_types")
+ {:ok, _view, html} = live(conn, "/membership_fee_settings")
# Should show the page (admin user in setup)
- assert html =~ "Membership Fee Types" || html =~ "Beitragsarten"
+ assert html =~ "Membership Fee Settings" || html =~ "Beitragseinstellungen" ||
+ html =~ "Membership Fee Types"
end
end
end
diff --git a/test/mv_web/plugs/check_page_permission_test.exs b/test/mv_web/plugs/check_page_permission_test.exs
index e342744..6dd8022 100644
--- a/test/mv_web/plugs/check_page_permission_test.exs
+++ b/test/mv_web/plugs/check_page_permission_test.exs
@@ -279,17 +279,11 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
end
@tag role: :member
- test "GET /membership_fee_types redirects to user profile", %{conn: conn, current_user: user} do
- conn = get(conn, "/membership_fee_types")
- assert redirected_to(conn) == "/users/#{user.id}"
- end
-
- @tag role: :member
- test "GET /membership_fee_types/new redirects to user profile", %{
+ test "GET /membership_fee_settings/new_fee_type redirects to user profile", %{
conn: conn,
current_user: user
} do
- conn = get(conn, "/membership_fee_types/new")
+ conn = get(conn, "/membership_fee_settings/new_fee_type")
assert redirected_to(conn) == "/users/#{user.id}"
end
@@ -385,7 +379,7 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
end
@tag role: :member
- test "GET /membership_fee_types/:id/edit redirects to user profile", %{
+ test "GET /membership_fee_settings/:id/edit_fee_type redirects to user profile", %{
conn: conn,
current_user: user
} do
@@ -396,7 +390,7 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
|> List.first()
if type do
- conn = get(conn, "/membership_fee_types/#{type.id}/edit")
+ conn = get(conn, "/membership_fee_settings/#{type.id}/edit_fee_type")
assert redirected_to(conn) == "/users/#{user.id}"
end
end
@@ -680,15 +674,6 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert redirected_to(conn) == "/users/#{user.id}"
end
- @tag role: :read_only
- test "GET /membership_fee_types redirects to user profile", %{
- conn: conn,
- current_user: user
- } do
- conn = get(conn, "/membership_fee_types")
- assert redirected_to(conn) == "/users/#{user.id}"
- end
-
@tag role: :read_only
test "GET /groups/new redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/groups/new")
@@ -864,15 +849,6 @@ defmodule MvWeb.Plugs.CheckPagePermissionTest do
assert redirected_to(conn) == "/users/#{user.id}"
end
- @tag role: :normal_user
- test "GET /membership_fee_types redirects to user profile", %{
- conn: conn,
- current_user: user
- } do
- conn = get(conn, "/membership_fee_types")
- assert redirected_to(conn) == "/users/#{user.id}"
- end
-
@tag role: :normal_user
test "GET /admin/roles redirects to user profile", %{conn: conn, current_user: user} do
conn = get(conn, "/admin/roles")