Compare commits

..

2 commits

Author SHA1 Message Date
9cda832b82
fix: request scopes email and profile
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 22:02:23 +01:00
613a5f2643
feat: support email scope to retrieve oidc info
All checks were successful
continuous-integration/drone/push Build is passing
2025-12-03 21:51:12 +01:00
7 changed files with 78 additions and 165 deletions

View file

@ -54,6 +54,9 @@ defmodule Mv.Accounts.User do
auth_method :client_secret_jwt auth_method :client_secret_jwt
code_verifier true 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 # id_token_signed_response_alg "EdDSA" #-> https://git.local-it.org/local-it/mitgliederverwaltung/issues/87
end end
@ -69,7 +72,7 @@ defmodule Mv.Accounts.User do
# Default actions for framework/tooling integration: # Default actions for framework/tooling integration:
# - :read -> Standard read used across the app and by admin tooling. # - :read -> Standard read used across the app and by admin tooling.
# - :destroy-> Standard delete used by admin tooling and maintenance tasks. # - :destroy-> Standard delete used by admin tooling and maintenance tasks.
# #
# NOTE: :create is INTENTIONALLY excluded from defaults! # NOTE: :create is INTENTIONALLY excluded from defaults!
# Using a default :create would bypass email-synchronization logic. # Using a default :create would bypass email-synchronization logic.
# Always use one of these explicit create actions instead: # Always use one of these explicit create actions instead:
@ -185,7 +188,9 @@ defmodule Mv.Accounts.User do
oidc_user_info = Ash.Changeset.get_argument(changeset, :oidc_user_info) oidc_user_info = Ash.Changeset.get_argument(changeset, :oidc_user_info)
# Get the new email from OIDC user_info # Get the new email from OIDC user_info
new_email = Map.get(oidc_user_info, "preferred_username") # Support both "email" (standard OIDC) and "preferred_username" (Rauthy)
new_email =
Map.get(oidc_user_info, "email") || Map.get(oidc_user_info, "preferred_username")
changeset changeset
|> Ash.Changeset.change_attribute(:oidc_id, oidc_id) |> Ash.Changeset.change_attribute(:oidc_id, oidc_id)
@ -239,8 +244,11 @@ defmodule Mv.Accounts.User do
change fn changeset, _ctx -> change fn changeset, _ctx ->
user_info = Ash.Changeset.get_argument(changeset, :user_info) 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 changeset
|> Ash.Changeset.change_attribute(:email, user_info["preferred_username"]) |> Ash.Changeset.change_attribute(:email, email)
|> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"]) |> Ash.Changeset.change_attribute(:oidc_id, user_info["sub"] || user_info["id"])
end end

View file

@ -10,7 +10,6 @@ defmodule MvWeb.Layouts do
use MvWeb, :html use MvWeb, :html
use Gettext, backend: MvWeb.Gettext use Gettext, backend: MvWeb.Gettext
import MvWeb.Layouts.Navbar import MvWeb.Layouts.Navbar
import MvWeb.Layouts.Sidebar
embed_templates "layouts/*" embed_templates "layouts/*"
@ -40,39 +39,20 @@ defmodule MvWeb.Layouts do
slot :inner_block, required: true slot :inner_block, required: true
def app(assigns) do def app(assigns) do
club_name = get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H""" ~H"""
<div class="drawer lg:drawer-open"> <%= if @current_user do %>
<input id="main-drawer" type="checkbox" class="drawer-toggle" /> <.navbar current_user={@current_user} />
<div class="drawer-content"> <% end %>
<%= if @current_user do %> <main class="px-4 py-20 sm:px-6 lg:px-16">
<.navbar current_user={@current_user} /> <div class="mx-auto max-full space-y-4">
<% end %> {render_slot(@inner_block)}
<main class="px-4 py-20 sm:px-6 lg:px-16">
<div class="mx-auto space-y-4 max-full">
{render_slot(@inner_block)}
</div>
</main>
</div> </div>
</main>
<.sidebar current_user={@current_user} club_name={@club_name} />
</div>
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />
""" """
end end
# Helper function to get club name from settings
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
defp get_club_name do
case Mv.Membership.get_settings() do
{:ok, settings} -> settings.club_name
_ -> "Mitgliederverwaltung"
end
end
@doc """ @doc """
Shows the flash group with standard titles and content. Shows the flash group with standard titles and content.
@ -85,7 +65,7 @@ defmodule MvWeb.Layouts do
def flash_group(assigns) do def flash_group(assigns) do
~H""" ~H"""
<div id={@id} aria-live="polite" class="z-50 flex flex-col gap-2 toast toast-top toast-end"> <div id={@id} aria-live="polite" class="toast toast-top toast-end z-50 flex flex-col gap-2">
<.flash kind={:success} flash={@flash} /> <.flash kind={:success} flash={@flash} />
<.flash kind={:warning} flash={@flash} /> <.flash kind={:warning} flash={@flash} />
<.flash kind={:info} flash={@flash} /> <.flash kind={:info} flash={@flash} />

View file

@ -6,35 +6,37 @@ defmodule MvWeb.Layouts.Navbar do
use Gettext, backend: MvWeb.Gettext use Gettext, backend: MvWeb.Gettext
use MvWeb, :verified_routes use MvWeb, :verified_routes
alias Mv.Membership
attr :current_user, :map, attr :current_user, :map,
required: true, required: true,
doc: "The current user - navbar is only shown when user is present" doc: "The current user - navbar is only shown when user is present"
def navbar(assigns) do def navbar(assigns) do
club_name = get_club_name()
assigns = assign(assigns, :club_name, club_name)
~H""" ~H"""
<header class="shadow-sm navbar bg-base-100"> <header class="navbar bg-base-100 shadow-sm">
<div class="flex-1"> <div class="flex-1">
<label <a class="btn btn-ghost text-xl">{@club_name}</a>
for="main-drawer" <ul class="menu menu-horizontal bg-base-200">
aria-label="open sidebar" <li><.link navigate="/members">{gettext("Members")}</.link></li>
class="mr-2 btn btn-square btn-ghost lg:hidden" <li><.link navigate="/settings">{gettext("Settings")}</.link></li>
> <li><.link navigate="/users">{gettext("Users")}</.link></li>
<svg <li>
xmlns="http://www.w3.org/2000/svg" <details>
viewBox="0 0 24 24" <summary>{gettext("Contributions")}</summary>
stroke-linejoin="round" <ul class="bg-base-200 rounded-t-none p-2 z-10 w-48">
stroke-linecap="round" <li><.link navigate="/contribution_types">{gettext("Contribution Types")}</.link></li>
stroke-width="2" <li>
fill="none" <.link navigate="/contribution_settings">{gettext("Contribution Settings")}</.link>
stroke="currentColor" </li>
class="my-1.5 inline-block size-4" </ul>
> </details>
<path d="M4 4m0 2a2 2 0 0 1 2 -2h12a2 2 0 0 1 2 2v12a2 2 0 0 1 -2 2h-12a2 2 0 0 1 -2 -2z"> </li>
</path> </ul>
<path d="M9 4v16"></path>
<path d="M14 10l2 2l-2 2"></path>
</svg>
</label>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<form method="post" action="/set_locale" class="mr-4"> <form method="post" action="/set_locale" class="mr-4">
@ -52,7 +54,7 @@ defmodule MvWeb.Layouts.Navbar do
</select> </select>
</form> </form>
<!-- Daisy UI Theme Toggle for dark and light mode--> <!-- Daisy UI Theme Toggle for dark and light mode-->
<label class="flex gap-2 cursor-pointer" aria-label={gettext("Toggle dark mode")}> <label class="flex cursor-pointer gap-2" aria-label={gettext("Toggle dark mode")}>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@ -91,19 +93,22 @@ defmodule MvWeb.Layouts.Navbar do
</label> </label>
<div class="dropdown dropdown-end"> <div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar avatar-placeholder"> <div tabindex="0" role="button" class="btn btn-ghost btn-circle avatar avatar-placeholder">
<div class="w-12 rounded-full bg-neutral text-neutral-content"> <div class="bg-neutral text-neutral-content w-12 rounded-full">
<span>AA</span> <span>AA</span>
</div> </div>
</div> </div>
<ul <ul
tabindex="0" tabindex="0"
class="p-2 mt-3 shadow menu menu-sm dropdown-content bg-base-100 rounded-box z-1 w-52" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow"
> >
<li> <li>
<.link navigate={~p"/users/#{@current_user.id}"}> <.link navigate={~p"/users/#{@current_user.id}"}>
{gettext("Profil")} {gettext("Profil")}
</.link> </.link>
</li> </li>
<li>
<.link navigate={~p"/settings"}>{gettext("Settings")}</.link>
</li>
<li> <li>
<.link href={~p"/sign-out"}>{gettext("Logout")}</.link> <.link href={~p"/sign-out"}>{gettext("Logout")}</.link>
</li> </li>
@ -113,4 +118,13 @@ defmodule MvWeb.Layouts.Navbar do
</header> </header>
""" """
end end
# Helper function to get club name from settings
# Falls back to "Mitgliederverwaltung" if settings can't be loaded
defp get_club_name do
case Membership.get_settings() do
{:ok, settings} -> settings.club_name
_ -> "Mitgliederverwaltung"
end
end
end end

View file

@ -1,80 +0,0 @@
defmodule MvWeb.Layouts.Sidebar do
@moduledoc """
Sidebar navigation component used in the drawer layout
"""
use MvWeb, :html
attr :current_user, :map, default: nil, doc: "The current user"
attr :club_name, :string, required: true, doc: "The name of the club"
def sidebar(assigns) do
~H"""
<div class="drawer-side is-drawer-close:overflow-visible">
<label for="main-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
<div class="flex flex-col items-start min-h-full bg-base-200 is-drawer-close:w-14 is-drawer-open:w-64">
<ul class="w-64 menu">
<li>
<h1 class="mb-2 text-lg font-bold menu-title is-drawer-close:hidden">{@club_name}</h1>
</li>
<%= if @current_user do %>
<li>
<.link
navigate="/members"
class={[
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
]}
data-tip={gettext("Members")}
>
<.icon name="hero-users" class="size-5" />
<span class="is-drawer-close:hidden">{gettext("Members")}</span>
</.link>
</li>
<li>
<.link
navigate="/users"
class={[
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
]}
data-tip={gettext("Users")}
>
<.icon name="hero-user-circle" class="size-5" />
<span class="is-drawer-close:hidden">{gettext("Users")}</span>
</.link>
</li>
<li class="is-drawer-close:hidden">
<h2 class="flex items-center gap-2 menu-title">
<.icon name="hero-currency-dollar" class="size-5" />
{gettext("Contributions")}
</h2>
<ul>
<li class="is-drawer-close:hidden">
<.link navigate="/contribution_types">
{gettext("Plans")}
</.link>
</li>
<li class="is-drawer-close:hidden">
<.link navigate="/contribution_settings">
{gettext("Settings")}
</.link>
</li>
</ul>
</li>
<li>
<.link
navigate="/settings"
class={[
"is-drawer-close:tooltip is-drawer-close:tooltip-right"
]}
data-tip={gettext("Settings")}
>
<.icon name="hero-cog-6-tooth" class="size-5" />
<span class="is-drawer-close:hidden">{gettext("Settings")}</span>
</.link>
</li>
<% end %>
</ul>
</div>
</div>
"""
end
end

View file

@ -292,7 +292,7 @@ msgstr "Benutzer*innen auflisten"
msgid "Member" msgid "Member"
msgstr "Mitglied" msgstr "Mitglied"
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -348,7 +348,7 @@ msgstr "Alle Mitglieder auswählen"
msgid "Select member" msgid "Select member"
msgstr "Mitglied auswählen" msgstr "Mitglied auswählen"
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -531,7 +531,7 @@ msgstr "Dunklen Modus umschalten"
msgid "Search..." msgid "Search..."
msgstr "Suchen..." msgstr "Suchen..."
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
msgstr "Benutzer*innen" msgstr "Benutzer*innen"
@ -943,16 +943,18 @@ msgstr ""
msgid "Configure global settings for membership contributions." msgid "Configure global settings for membership contributions."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_settings_live.ex #: lib/mv_web/live/contribution_settings_live.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Settings" msgid "Contribution Settings"
msgstr "Beitragseinstellungen" msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex #: lib/mv_web/live/contribution_period_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Start" msgid "Contribution Start"
msgstr "Beitrag" msgstr "Beitrag"
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contribution Types" msgid "Contribution Types"
@ -973,10 +975,10 @@ msgstr "Beitrag"
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Contributions" msgid "Contributions"
msgstr "Beiträge" msgstr "Beitrag"
#: lib/mv_web/live/contribution_period_live/show.ex #: lib/mv_web/live/contribution_period_live/show.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
@ -1403,11 +1405,6 @@ msgstr "Diese Felder können zusätzlich zu den normalen Daten ausgefüllt werde
msgid "Value Type" msgid "Value Type"
msgstr "Wertetyp" msgstr "Wertetyp"
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Plans"
msgstr "Beitragsklassen"
#~ #: lib/mv_web/live/custom_field_live/show.ex #~ #: lib/mv_web/live/custom_field_live/show.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Auto-generated identifier (immutable)" #~ msgid "Auto-generated identifier (immutable)"

View file

@ -293,7 +293,7 @@ msgstr ""
msgid "Member" msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -349,7 +349,7 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -532,7 +532,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Users" msgid "Users"
msgstr "" msgstr ""
@ -944,6 +944,7 @@ msgstr ""
msgid "Configure global settings for membership contributions." msgid "Configure global settings for membership contributions."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_settings_live.ex #: lib/mv_web/live/contribution_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Settings" msgid "Contribution Settings"
@ -954,6 +955,7 @@ msgstr ""
msgid "Contribution Start" msgid "Contribution Start"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Types" msgid "Contribution Types"
@ -974,7 +976,7 @@ msgstr ""
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contributions" msgid "Contributions"
msgstr "" msgstr ""
@ -1403,8 +1405,3 @@ msgstr ""
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Value Type" msgid "Value Type"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Plans"
msgstr ""

View file

@ -293,7 +293,7 @@ msgstr ""
msgid "Member" msgid "Member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#: lib/mv_web/live/member_live/index.ex #: lib/mv_web/live/member_live/index.ex
#: lib/mv_web/live/member_live/index.html.heex #: lib/mv_web/live/member_live/index.html.heex
@ -349,7 +349,7 @@ msgstr ""
msgid "Select member" msgid "Select member"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/global_settings_live.ex #: lib/mv_web/live/global_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Settings" msgid "Settings"
@ -532,7 +532,7 @@ msgstr ""
msgid "Search..." msgid "Search..."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format, fuzzy #, elixir-autogen, elixir-format, fuzzy
msgid "Users" msgid "Users"
msgstr "" msgstr ""
@ -944,6 +944,7 @@ msgstr ""
msgid "Configure global settings for membership contributions." msgid "Configure global settings for membership contributions."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_settings_live.ex #: lib/mv_web/live/contribution_settings_live.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Settings" msgid "Contribution Settings"
@ -954,6 +955,7 @@ msgstr ""
msgid "Contribution Start" msgid "Contribution Start"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/navbar.ex
#: lib/mv_web/live/contribution_type_live/index.ex #: lib/mv_web/live/contribution_type_live/index.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contribution Types" msgid "Contribution Types"
@ -974,7 +976,7 @@ msgstr ""
msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation." msgid "Contribution types define different membership fee structures. Each type has a fixed interval (monthly, quarterly, half-yearly, yearly) that cannot be changed after creation."
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex #: lib/mv_web/components/layouts/navbar.ex
#, elixir-autogen, elixir-format #, elixir-autogen, elixir-format
msgid "Contributions" msgid "Contributions"
msgstr "" msgstr ""
@ -1404,11 +1406,6 @@ msgstr ""
msgid "Value Type" msgid "Value Type"
msgstr "" msgstr ""
#: lib/mv_web/components/layouts/sidebar.ex
#, elixir-autogen, elixir-format
msgid "Plans"
msgstr ""
#~ #: lib/mv_web/live/custom_field_live/show.ex #~ #: lib/mv_web/live/custom_field_live/show.ex
#~ #, elixir-autogen, elixir-format #~ #, elixir-autogen, elixir-format
#~ msgid "Auto-generated identifier (immutable)" #~ msgid "Auto-generated identifier (immutable)"