diff --git a/.drone.yml b/.drone.yml index 483a08a..5fbb73b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -4,7 +4,7 @@ name: check services: - name: postgres - image: docker.io/library/postgres:17.6 + image: docker.io/library/postgres:18.1 environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres @@ -57,7 +57,7 @@ steps: - mix gettext.extract --check-up-to-date - name: wait_for_postgres - image: docker.io/library/postgres:17.6 + image: docker.io/library/postgres:18.1 commands: # Wait for postgres to become available - | @@ -166,7 +166,7 @@ environment: steps: - name: renovate - image: renovate/renovate:41.173 + image: renovate/renovate:41.151 environment: RENOVATE_CONFIG_FILE: "renovate_backend_config.js" RENOVATE_TOKEN: diff --git a/.tool-versions b/.tool-versions index 98239f3..60315fc 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.18.3-otp-27 erlang 27.3.4 -just 1.43.1 +just 1.43.0 diff --git a/Justfile b/Justfile index b835cf4..25fb35c 100644 --- a/Justfile +++ b/Justfile @@ -1,7 +1,4 @@ set dotenv-load := true -set export := true - -MIX_QUIET := "1" run: install-dependencies start-database migrate-database seed-database mix phx.server @@ -93,7 +90,7 @@ clean: remove-gettext-conflicts: #!/usr/bin/env bash set -euo pipefail - find priv/gettext -type f -exec sed -i '/^<<<<<<>>>>>>/d; /^%%%%%%%/d; /^++++++/d; s/^+//; s/^-//' {} \; + find priv/gettext -type f -exec sed -i '/^<<<<<<>>>>>>/d; /^%%%%%%%/d; /^++++++/d; s/^+//' {} \; # Production environment commands # ================================ diff --git a/README.md b/README.md index 090f4e9..14435db 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Our philosophy: **software should help people spend less time on administration - 🚧 Sorting & filtering - 🚧 Roles & permissions (e.g. board, treasurer) - ✅ Custom fields (flexible per club needs) -- ✅ SSO via OIDC (works with Authentik, Rauthy, Keycloak, etc.) +- ✅ SSO via OIDC (tested with Rauthy) - 🚧 Self-service & online application - 🚧 Accessibility, GDPR, usability improvements - 🚧 Email sending @@ -147,26 +147,7 @@ Mila uses OIDC for Single Sign-On. In development, a local **Rauthy** instance i 5. copy client secret to `.env` file 6. abort and run `just run` again -Now you can log in to Mila via OIDC! - -### OIDC with other providers (Authentik, Keycloak, etc.) - -Mila works with any OIDC-compliant provider. The internal strategy is named `:rauthy`, but this is just a name — it works with any provider. - -**Important:** The redirect URI must always end with `/auth/user/rauthy/callback`. - -Example for Authentik: -1. Create an OAuth2/OpenID Provider in Authentik -2. Set the redirect URI to: `https://your-domain.com/auth/user/rauthy/callback` -3. Configure environment variables: - ```bash - DOMAIN=your-domain.com # or PHX_HOST=your-domain.com - OIDC_CLIENT_ID=your-client-id - OIDC_BASE_URL=https://auth.example.com/application/o/your-app - OIDC_CLIENT_SECRET=your-client-secret # or use OIDC_CLIENT_SECRET_FILE - ``` - -The `OIDC_REDIRECT_URI` is auto-generated as `https://{DOMAIN}/auth/user/rauthy/callback` if not explicitly set. +Now you can log in to Mila via OIDC! ## ⚙️ Configuration @@ -229,13 +210,13 @@ For testing the production Docker build locally: # Required variables: SECRET_KEY_BASE= TOKEN_SIGNING_SECRET= - DOMAIN=localhost # or PHX_HOST=localhost + PHX_HOST=localhost - # Optional OIDC configuration: + # Optional (have defaults in docker-compose.prod.yml): # OIDC_CLIENT_ID=mv # OIDC_BASE_URL=http://localhost:8080/auth/v1 - # OIDC_CLIENT_SECRET= - # OIDC_REDIRECT_URI is auto-generated as https://{DOMAIN}/auth/user/rauthy/callback + # OIDC_REDIRECT_URI=http://localhost:4001/auth/user/rauthy/callback + # OIDC_CLIENT_SECRET= # Alternative: Use _FILE variables for Docker secrets (takes priority over regular vars): # SECRET_KEY_BASE_FILE=/run/secrets/secret_key_base diff --git a/config/runtime.exs b/config/runtime.exs index 06a2cd8..71138ef 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -112,21 +112,12 @@ if config_env() == :prod do You can generate one by calling: mix phx.gen.secret """) - # PHX_HOST or DOMAIN can be used to set the host for the application. - # DOMAIN is commonly used in deployment environments (e.g., Portainer templates). - host = - System.get_env("PHX_HOST") || - System.get_env("DOMAIN") || - raise "Please define the PHX_HOST or DOMAIN environment variable." - + host = System.get_env("PHX_HOST") || raise "Please define the PHX_HOST environment variable." port = String.to_integer(System.get_env("PORT") || "4000") config :mv, :dns_cluster_query, System.get_env("DNS_CLUSTER_QUERY") - # OIDC configuration (works with any OIDC provider: Authentik, Rauthy, Keycloak, etc.) - # Note: The strategy is named :rauthy internally, but works with any OIDC provider. - # The redirect_uri callback path is always /auth/user/rauthy/callback regardless of provider. - # + # Rauthy OIDC configuration # Supports OIDC_CLIENT_SECRET or OIDC_CLIENT_SECRET_FILE for Docker secrets. # OIDC_CLIENT_SECRET is required only if OIDC is being used (indicated by explicit OIDC env vars). oidc_base_url = System.get_env("OIDC_BASE_URL") @@ -143,15 +134,12 @@ if config_env() == :prod do get_env_or_file.("OIDC_CLIENT_SECRET", nil) end - # Build redirect_uri: use OIDC_REDIRECT_URI if set, otherwise build from host. - # Uses HTTPS since production runs behind TLS termination. - default_redirect_uri = "https://#{host}/auth/user/rauthy/callback" - config :mv, :rauthy, client_id: oidc_client_id || "mv", base_url: oidc_base_url || "http://localhost:8080/auth/v1", client_secret: client_secret, - redirect_uri: System.get_env("OIDC_REDIRECT_URI") || default_redirect_uri + redirect_uri: + System.get_env("OIDC_REDIRECT_URI") || "http://#{host}:#{port}/auth/user/rauthy/callback" # Token signing secret from environment variable # This overrides the placeholder value set in prod.exs diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index b4b7a1f..bc2ca7d 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -33,7 +33,7 @@ services: restart: unless-stopped db-prod: - image: postgres:16-alpine + image: postgres:18-alpine container_name: mv-prod-db environment: POSTGRES_USER: postgres diff --git a/docker-compose.yml b/docker-compose.yml index 56876f2..8bc5db6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ networks: services: db: - image: postgres:17.6-alpine + image: postgres:18.1-alpine environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres diff --git a/lib/accounts/user.ex b/lib/accounts/user.ex index dbc62b2..749740d 100644 --- a/lib/accounts/user.ex +++ b/lib/accounts/user.ex @@ -54,9 +54,6 @@ defmodule Mv.Accounts.User do auth_method :client_secret_jwt code_verifier true - # Request email and profile scopes from OIDC provider (required for Authentik, Keycloak, etc.) - authorization_params scope: "openid email profile" - # id_token_signed_response_alg "EdDSA" #-> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87 end @@ -72,7 +69,7 @@ defmodule Mv.Accounts.User do # Default actions for framework/tooling integration: # - :read -> Standard read used across the app and by admin tooling. # - :destroy-> Standard delete used by admin tooling and maintenance tasks. - # + # # NOTE: :create is INTENTIONALLY excluded from defaults! # Using a default :create would bypass email-synchronization logic. # Always use one of these explicit create actions instead: @@ -188,9 +185,7 @@ defmodule Mv.Accounts.User do oidc_user_info = Ash.Changeset.get_argument(changeset, :oidc_user_info) # Get the new email from OIDC user_info - # Support both "email" (standard OIDC) and "preferred_username" (Rauthy) - new_email = - Map.get(oidc_user_info, "email") || Map.get(oidc_user_info, "preferred_username") + new_email = Map.get(oidc_user_info, "preferred_username") changeset |> Ash.Changeset.change_attribute(:oidc_id, oidc_id) @@ -244,11 +239,8 @@ defmodule Mv.Accounts.User do change fn changeset, _ctx -> user_info = Ash.Changeset.get_argument(changeset, :user_info) - # Support both "email" (standard OIDC like Authentik, Keycloak) and "preferred_username" (Rauthy) - email = user_info["email"] || user_info["preferred_username"] - changeset - |> Ash.Changeset.change_attribute(:email, email) + |> Ash.Changeset.change_attribute(:email, user_info["preferred_username"]) |> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"]) end diff --git a/lib/membership/member.ex b/lib/membership/member.ex index b788dc9..8d271d7 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -401,70 +401,6 @@ defmodule Mv.Membership.Member do identity :unique_email, [:email] end - @doc """ - Checks if a member field should be shown in the overview. - - Reads the visibility configuration from Settings resource. If a field is not - configured in settings, it defaults to `true` (visible). - - ## Parameters - - `field` - Atom representing the member field name (e.g., `:email`, `:street`) - - ## Returns - - `true` if the field should be shown in overview (default) - - `false` if the field is configured as hidden in settings - - ## Examples - - iex> Member.show_in_overview?(:email) - true - - iex> Member.show_in_overview?(:street) - true # or false if configured in settings - - """ - @spec show_in_overview?(atom()) :: boolean() - def show_in_overview?(field) when is_atom(field) do - case Mv.Membership.get_settings() do - {:ok, settings} -> - visibility_config = settings.member_field_visibility || %{} - # Normalize map keys to atoms (JSONB may return string keys) - normalized_config = normalize_visibility_config(visibility_config) - - # Get value from normalized config, default to true - Map.get(normalized_config, field, true) - - {:error, _} -> - # If settings can't be loaded, default to visible - true - end - end - - def show_in_overview?(_), do: true - - # Normalizes visibility config map keys from strings to atoms. - # JSONB in PostgreSQL converts atom keys to string keys when storing. - defp normalize_visibility_config(config) when is_map(config) do - Enum.reduce(config, %{}, fn - {key, value}, acc when is_atom(key) -> - Map.put(acc, key, value) - - {key, value}, acc when is_binary(key) -> - try do - atom_key = String.to_existing_atom(key) - Map.put(acc, atom_key, value) - rescue - ArgumentError -> - acc - end - - _, acc -> - acc - end) - end - - defp normalize_visibility_config(_), do: %{} - @doc """ Performs fuzzy search on members using PostgreSQL trigram similarity. diff --git a/lib/mv/constants.ex b/lib/mv/constants.ex index 7bfb07b..334bcc1 100644 --- a/lib/mv/constants.ex +++ b/lib/mv/constants.ex @@ -18,17 +18,5 @@ defmodule Mv.Constants do :postal_code ] - @custom_field_prefix "custom_field_" - def member_fields, do: @member_fields - - @doc """ - Returns the prefix used for custom field keys in field visibility maps. - - ## Examples - - iex> Mv.Constants.custom_field_prefix() - "custom_field_" - """ - def custom_field_prefix, do: @custom_field_prefix end diff --git a/lib/mv_web/components/core_components.ex b/lib/mv_web/components/core_components.ex index be64655..08133b5 100644 --- a/lib/mv_web/components/core_components.ex +++ b/lib/mv_web/components/core_components.ex @@ -119,123 +119,6 @@ defmodule MvWeb.CoreComponents do end end - @doc """ - Renders a dropdown menu. - - ## Examples - - <.dropdown_menu items={@items} open={@open} phx_target={@myself} /> - """ - attr :id, :string, default: "dropdown-menu" - attr :items, :list, required: true, doc: "List of %{label: string, value: any} maps" - attr :button_label, :string, default: "Dropdown" - attr :icon, :string, default: nil - attr :checkboxes, :boolean, default: false - attr :selected, :map, default: %{} - attr :open, :boolean, default: false, doc: "Whether the dropdown is open" - attr :show_select_buttons, :boolean, default: false, doc: "Show select all/none buttons" - attr :phx_target, :any, required: true, doc: "The LiveView/LiveComponent target for events" - - def dropdown_menu(assigns) do - ~H""" -
- - - -
- """ - end - @doc """ Renders an input with label and error messages. diff --git a/lib/mv_web/components/layouts/navbar.ex b/lib/mv_web/components/layouts/navbar.ex index 4246c99..1c1138e 100644 --- a/lib/mv_web/components/layouts/navbar.ex +++ b/lib/mv_web/components/layouts/navbar.ex @@ -23,7 +23,7 @@ defmodule MvWeb.Layouts.Navbar do {@club_name}