Merge branch 'main' into feat/299_plz
This commit is contained in:
commit
c8d7dd3e55
36 changed files with 250 additions and 187 deletions
|
|
@ -9,7 +9,7 @@ defmodule Mv.Accounts do
|
|||
## Public API
|
||||
The domain exposes these main actions:
|
||||
- User CRUD: `create_user/1`, `list_users/0`, `update_user/2`, `destroy_user/1`
|
||||
- Authentication: `create_register_with_rauthy/1`, `read_sign_in_with_rauthy/1`
|
||||
- Authentication: `create_register_with_oidc/1`, `read_sign_in_with_oidc/1`
|
||||
"""
|
||||
use Ash.Domain,
|
||||
extensions: [AshAdmin.Domain, AshPhoenix]
|
||||
|
|
@ -24,8 +24,8 @@ defmodule Mv.Accounts do
|
|||
define :list_users, action: :read
|
||||
define :update_user, action: :update_user
|
||||
define :destroy_user, action: :destroy
|
||||
define :create_register_with_rauthy, action: :register_with_rauthy
|
||||
define :read_sign_in_with_rauthy, action: :sign_in_with_rauthy
|
||||
define :create_register_with_oidc, action: :register_with_oidc
|
||||
define :read_sign_in_with_oidc, action: :sign_in_with_oidc
|
||||
end
|
||||
|
||||
resource Mv.Accounts.Token
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ defmodule Mv.Accounts.User do
|
|||
|
||||
@doc """
|
||||
AshAuthentication specific: Defines the strategies we want to use for authentication.
|
||||
Currently password and SSO with Rauthy as OIDC provider
|
||||
Currently password and SSO via OIDC (supports any provider: Authentik, Rauthy, Keycloak, etc.)
|
||||
"""
|
||||
authentication do
|
||||
session_identifier Application.compile_env!(:mv, :session_identifier)
|
||||
|
|
@ -52,7 +52,7 @@ defmodule Mv.Accounts.User do
|
|||
end
|
||||
|
||||
strategies do
|
||||
oidc :rauthy do
|
||||
oidc :oidc do
|
||||
client_id Mv.Secrets
|
||||
base_url Mv.Secrets
|
||||
redirect_uri Mv.Secrets
|
||||
|
|
@ -88,7 +88,7 @@ defmodule Mv.Accounts.User do
|
|||
# Always use one of these explicit create actions instead:
|
||||
# - :create_user (for manual user creation with optional member link)
|
||||
# - :register_with_password (for password-based registration)
|
||||
# - :register_with_rauthy (for OIDC-based registration)
|
||||
# - :register_with_oidc (for OIDC-based registration)
|
||||
defaults [:read]
|
||||
|
||||
destroy :destroy do
|
||||
|
|
@ -267,7 +267,7 @@ defmodule Mv.Accounts.User do
|
|||
prepare AshAuthentication.Preparations.FilterBySubject
|
||||
end
|
||||
|
||||
read :sign_in_with_rauthy do
|
||||
read :sign_in_with_oidc do
|
||||
# Single record expected; required for AshAuthentication OAuth2 strategy (returns list of 0 or 1).
|
||||
get? true
|
||||
argument :user_info, :map, allow_nil?: false
|
||||
|
|
@ -302,7 +302,7 @@ defmodule Mv.Accounts.User do
|
|||
end)
|
||||
end
|
||||
|
||||
create :register_with_rauthy do
|
||||
create :register_with_oidc do
|
||||
argument :user_info, :map, allow_nil?: false
|
||||
argument :oauth_tokens, :map, allow_nil?: false
|
||||
upsert? true
|
||||
|
|
|
|||
|
|
@ -52,7 +52,8 @@ defmodule Mv.Membership.CustomField do
|
|||
use Ash.Resource,
|
||||
domain: Mv.Membership,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
authorizers: [Ash.Policy.Authorizer],
|
||||
primary_read_warning?: false
|
||||
|
||||
postgres do
|
||||
table "custom_fields"
|
||||
|
|
@ -60,9 +61,13 @@ defmodule Mv.Membership.CustomField do
|
|||
end
|
||||
|
||||
actions do
|
||||
defaults [:read]
|
||||
default_accept [:name, :value_type, :description, :required, :show_in_overview]
|
||||
|
||||
read :read do
|
||||
primary? true
|
||||
prepare build(sort: [name: :asc])
|
||||
end
|
||||
|
||||
create :create do
|
||||
accept [:name, :value_type, :description, :required, :show_in_overview]
|
||||
change Mv.Membership.Changes.GenerateSlug
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ defmodule Mv.OidcRoleSync do
|
|||
@moduledoc """
|
||||
Syncs user role from OIDC user_info (e.g. groups claim → Admin role).
|
||||
|
||||
Used after OIDC registration (register_with_rauthy) and on sign-in so that
|
||||
Used after OIDC registration (register_with_oidc) and on sign-in so that
|
||||
users in the configured admin group get the Admin role; others get Mitglied.
|
||||
Configure via OIDC_ADMIN_GROUP_NAME and OIDC_GROUPS_CLAIM (see OidcRoleSyncConfig).
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ defmodule Mv.Secrets do
|
|||
particularly for OIDC (Rauthy) authentication.
|
||||
|
||||
## Configuration Source
|
||||
Secrets are read from the `:rauthy` key in the application configuration,
|
||||
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`
|
||||
|
|
@ -21,7 +21,7 @@ defmodule Mv.Secrets do
|
|||
use AshAuthentication.Secret
|
||||
|
||||
def secret_for(
|
||||
[:authentication, :strategies, :rauthy, :client_id],
|
||||
[:authentication, :strategies, :oidc, :client_id],
|
||||
Mv.Accounts.User,
|
||||
_opts,
|
||||
_meth
|
||||
|
|
@ -30,7 +30,7 @@ defmodule Mv.Secrets do
|
|||
end
|
||||
|
||||
def secret_for(
|
||||
[:authentication, :strategies, :rauthy, :redirect_uri],
|
||||
[:authentication, :strategies, :oidc, :redirect_uri],
|
||||
Mv.Accounts.User,
|
||||
_opts,
|
||||
_meth
|
||||
|
|
@ -39,7 +39,7 @@ defmodule Mv.Secrets do
|
|||
end
|
||||
|
||||
def secret_for(
|
||||
[:authentication, :strategies, :rauthy, :client_secret],
|
||||
[:authentication, :strategies, :oidc, :client_secret],
|
||||
Mv.Accounts.User,
|
||||
_opts,
|
||||
_meth
|
||||
|
|
@ -48,7 +48,7 @@ defmodule Mv.Secrets do
|
|||
end
|
||||
|
||||
def secret_for(
|
||||
[:authentication, :strategies, :rauthy, :base_url],
|
||||
[:authentication, :strategies, :oidc, :base_url],
|
||||
Mv.Accounts.User,
|
||||
_opts,
|
||||
_meth
|
||||
|
|
@ -58,7 +58,7 @@ defmodule Mv.Secrets do
|
|||
|
||||
defp get_config(key) do
|
||||
:mv
|
||||
|> Application.fetch_env!(:rauthy)
|
||||
|> Application.fetch_env!(:oidc)
|
||||
|> Keyword.fetch!(key)
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ defmodule MvWeb.Layouts do
|
|||
data-sidebar-expanded="true"
|
||||
phx-hook="SidebarState"
|
||||
>
|
||||
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" />
|
||||
<input id="mobile-drawer" type="checkbox" class="drawer-toggle" phx-update="ignore" />
|
||||
|
||||
<div class="drawer-content flex flex-col relative z-0">
|
||||
<!-- Mobile Header (only visible on mobile) -->
|
||||
|
|
|
|||
|
|
@ -48,8 +48,8 @@ defmodule MvWeb.AuthController do
|
|||
log_failure_safely(activity, reason)
|
||||
|
||||
case {activity, reason} do
|
||||
{{:rauthy, _action}, reason} ->
|
||||
handle_rauthy_failure(conn, reason)
|
||||
{{:oidc, _action}, reason} ->
|
||||
handle_oidc_failure(conn, reason)
|
||||
|
||||
{_, %AshAuthentication.Errors.AuthenticationFailed{caused_by: caused_by}} ->
|
||||
handle_authentication_failed(conn, caused_by)
|
||||
|
|
@ -61,8 +61,8 @@ defmodule MvWeb.AuthController do
|
|||
end
|
||||
end
|
||||
|
||||
# Log authentication failures safely, avoiding sensitive data for {:rauthy, _} activities
|
||||
defp log_failure_safely({:rauthy, _action} = activity, reason) do
|
||||
# Log authentication failures safely, avoiding sensitive data for {:oidc, _} activities
|
||||
defp log_failure_safely({:oidc, _action} = activity, reason) do
|
||||
# For Assent errors, use safe_assent_meta to avoid logging tokens/URLs with query params
|
||||
case reason do
|
||||
%Assent.ServerUnreachableError{} = err ->
|
||||
|
|
@ -76,7 +76,7 @@ defmodule MvWeb.AuthController do
|
|||
Logger.warning(message)
|
||||
|
||||
_ ->
|
||||
# For other rauthy errors, log only error type, not full details
|
||||
# For other OIDC errors, log only error type, not full details
|
||||
error_type = get_error_type(reason)
|
||||
|
||||
Logger.warning(
|
||||
|
|
@ -86,7 +86,7 @@ defmodule MvWeb.AuthController do
|
|||
end
|
||||
|
||||
defp log_failure_safely(activity, reason) do
|
||||
# For non-rauthy activities, safe to log full reason
|
||||
# For non-OIDC activities, safe to log full reason
|
||||
Logger.warning(
|
||||
"Authentication failure - Activity: #{inspect(activity)}, Reason: #{inspect(reason)}"
|
||||
)
|
||||
|
|
@ -119,12 +119,12 @@ defmodule MvWeb.AuthController do
|
|||
if Enum.empty?(parts), do: "", else: " - " <> Enum.join(parts, ", ")
|
||||
end
|
||||
|
||||
# Handle all Rauthy (OIDC) authentication failures
|
||||
defp handle_rauthy_failure(conn, %Ash.Error.Invalid{errors: errors}) do
|
||||
# Handle all OIDC authentication failures
|
||||
defp handle_oidc_failure(conn, %Ash.Error.Invalid{errors: errors}) do
|
||||
handle_oidc_email_collision(conn, errors)
|
||||
end
|
||||
|
||||
defp handle_rauthy_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{
|
||||
defp handle_oidc_failure(conn, %AshAuthentication.Errors.AuthenticationFailed{
|
||||
caused_by: caused_by
|
||||
}) do
|
||||
case caused_by do
|
||||
|
|
@ -139,7 +139,7 @@ defmodule MvWeb.AuthController do
|
|||
end
|
||||
|
||||
# Handle Assent server unreachable errors (network/connectivity issues)
|
||||
defp handle_rauthy_failure(conn, %Assent.ServerUnreachableError{} = _err) do
|
||||
defp handle_oidc_failure(conn, %Assent.ServerUnreachableError{} = _err) do
|
||||
# Logging already done safely in failure/3 via log_failure_safely/2
|
||||
# No need to log again here to avoid duplicate logs
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ defmodule MvWeb.AuthController do
|
|||
end
|
||||
|
||||
# Handle Assent invalid response errors (configuration or malformed responses)
|
||||
defp handle_rauthy_failure(conn, %Assent.InvalidResponseError{} = _err) do
|
||||
defp handle_oidc_failure(conn, %Assent.InvalidResponseError{} = _err) do
|
||||
# Logging already done safely in failure/3 via log_failure_safely/2
|
||||
# No need to log again here to avoid duplicate logs
|
||||
|
||||
|
|
@ -165,7 +165,7 @@ defmodule MvWeb.AuthController do
|
|||
end
|
||||
|
||||
# Catch-all clause for any other error types
|
||||
defp handle_rauthy_failure(conn, _reason) do
|
||||
defp handle_oidc_failure(conn, _reason) do
|
||||
# Logging already done safely in failure/3 via log_failure_safely/2
|
||||
# No need to log again here to avoid duplicate logs
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ defmodule MvWeb.LinkOidcAccountLive do
|
|||
:info,
|
||||
dgettext("auth", "Account activated! Redirecting to complete sign-in...")
|
||||
)
|
||||
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy")
|
||||
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/oidc")
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
|
|
@ -223,7 +223,7 @@ defmodule MvWeb.LinkOidcAccountLive do
|
|||
"Your OIDC account has been successfully linked! Redirecting to complete sign-in..."
|
||||
)
|
||||
)
|
||||
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/rauthy")}
|
||||
|> Phoenix.LiveView.redirect(to: ~p"/auth/user/oidc")}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
|
|
|
|||
|
|
@ -214,47 +214,49 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
|||
</:col>
|
||||
|
||||
<:action :let={cycle}>
|
||||
<div class="flex gap-1">
|
||||
<div class="flex gap-2">
|
||||
<%= if @can_update_cycle do %>
|
||||
<button
|
||||
:if={cycle.status != :paid}
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="paid"
|
||||
phx-target={@myself}
|
||||
class="btn btn-sm btn-success"
|
||||
title={gettext("Mark as paid")}
|
||||
>
|
||||
<.icon name="hero-check-circle" class="size-4" />
|
||||
{gettext("Paid")}
|
||||
</button>
|
||||
<button
|
||||
:if={cycle.status != :suspended}
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="suspended"
|
||||
phx-target={@myself}
|
||||
class="btn btn-sm btn-outline btn-warning"
|
||||
title={gettext("Mark as suspended")}
|
||||
>
|
||||
<.icon name="hero-pause-circle" class="size-4" />
|
||||
{gettext("Suspended")}
|
||||
</button>
|
||||
<button
|
||||
:if={cycle.status != :unpaid}
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="unpaid"
|
||||
phx-target={@myself}
|
||||
class="btn btn-sm btn-error"
|
||||
title={gettext("Mark as unpaid")}
|
||||
>
|
||||
<.icon name="hero-x-circle" class="size-4" />
|
||||
{gettext("Unpaid")}
|
||||
</button>
|
||||
<div class="join">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="paid"
|
||||
phx-target={@myself}
|
||||
class={cycle_status_btn_class(cycle.status, :paid)}
|
||||
aria-pressed={cycle.status == :paid}
|
||||
title={gettext("Mark as paid")}
|
||||
>
|
||||
<.icon name="hero-check-circle" class="size-4" />
|
||||
{gettext("Paid")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="suspended"
|
||||
phx-target={@myself}
|
||||
class={cycle_status_btn_class(cycle.status, :suspended)}
|
||||
aria-pressed={cycle.status == :suspended}
|
||||
title={gettext("Mark as suspended")}
|
||||
>
|
||||
<.icon name="hero-pause-circle" class="size-4" />
|
||||
{gettext("Suspended")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="mark_cycle_status"
|
||||
phx-value-cycle_id={cycle.id}
|
||||
phx-value-status="unpaid"
|
||||
phx-target={@myself}
|
||||
class={cycle_status_btn_class(cycle.status, :unpaid)}
|
||||
aria-pressed={cycle.status == :unpaid}
|
||||
title={gettext("Mark as unpaid")}
|
||||
>
|
||||
<.icon name="hero-x-circle" class="size-4" />
|
||||
{gettext("Unpaid")}
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if @can_destroy_cycle do %>
|
||||
<button
|
||||
|
|
@ -1219,6 +1221,20 @@ defmodule MvWeb.MemberLive.Show.MembershipFeesComponent do
|
|||
defp translate_receipt_type("income"), do: gettext("Income")
|
||||
defp translate_receipt_type(other), do: other
|
||||
|
||||
# Returns CSS classes for a cycle status button.
|
||||
# Active (current) status is highlighted with color and non-interactive;
|
||||
# inactive buttons are neutral gray. Matches the filter button pattern.
|
||||
defp cycle_status_btn_class(current_status, btn_status) do
|
||||
base = "join-item btn btn-sm"
|
||||
|
||||
case {current_status == btn_status, btn_status} do
|
||||
{true, :paid} -> "#{base} btn-success btn-active pointer-events-none"
|
||||
{true, :suspended} -> "#{base} btn-warning btn-active pointer-events-none"
|
||||
{true, :unpaid} -> "#{base} btn-error btn-active pointer-events-none"
|
||||
_ -> base
|
||||
end
|
||||
end
|
||||
|
||||
# Helper component for section box
|
||||
attr :title, :string, required: true
|
||||
slot :inner_block, required: true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue