All checks were successful
continuous-integration/drone/push Build is passing
## Description of the implemented changes The changes were: - [x] Bugfixing - [x] New Feature - [ ] Breaking Change - [x] Refactoring **OIDC-only mode improvements and UX tweaks (success toasts, unauthenticated redirect).** ## What has been changed? ### OIDC-only mode (new feature) - **Admin settings:** "Only OIDC sign-in" is an immediate toggle at the top of the OIDC section (no save button). Enabling it also turns off "Allow direct registration". When OIDC-only is on, the registration checkbox is disabled and shows a tooltip (DaisyUI `<.tooltip>`). - **Backend:** Password sign-in is forbidden via Ash policy (`OidcOnlyActive` check). Password registration is blocked via validation `OidcOnlyBlocksPasswordRegistration`. New plug `OidcOnlySignInRedirect`: when OIDC-only and OIDC are configured, GET `/sign-in` redirects to the OIDC flow; GET `/auth/user/password/sign_in_with_token` is rejected with redirect + flash. `AuthController.success/4` also rejects password sign-in when OIDC-only. - **Tests:** GlobalSettingsLive (OIDC-only UI), AuthController (redirect and password sign-in rejection), User authentication (register_with_password blocked when OIDC-only). ### UX / behaviour (no new feature flag) - **Success toasts:** Success flash messages auto-dismiss after 5 seconds via JS hook `FlashAutoDismiss` and optional `auto_clear_ms` on `<.flash>` (used for success in root layout and `flash_group`). - **Unauthenticated users:** Redirect to sign-in without the "You don't have permission to access this page" flash; that message is only shown to logged-in users who lack access. Logic in `LiveHelpers` and `CheckPagePermission` plug; test updated accordingly. ### Other - Layouts: comment about unprocessed join-request count no longer uses "TODO" (Credo). - Gettext: German translation for "Home" (Startseite); POT/PO kept in sync. - CHANGELOG: Unreleased section updated with the above. ## Definition of Done ### Code Quality - [x] No new technical depths - [x] Linting passed - [x] Documentation is added where needed (module docs, comments where non-obvious) ### Accessibility - [x] New elements are properly defined with html-tags (labels, aria-label on checkboxes) - [x] Colour contrast follows WCAG criteria (unchanged) - [x] Aria labels are added when needed (e.g. oidc-only and registration checkboxes) - [x] Everything is accessible by keyboard (toggles and buttons unchanged) - [x] Tab-Order is comprehensible - [x] All interactive elements have a visible focus (existing patterns) ### Testing - [x] Tests for new code are written (OIDC-only UI, auth controller, user auth; SMTP config builder and mailer) - [x] All tests pass - [ ] axe-core dev tools show no critical or major issues (not re-run for this PR; suggest spot-check on settings and sign-in) ## Additional Notes - **OIDC-only:** When the `OIDC_ONLY` env var is set, the toggle is read-only and shows "(From OIDC_ONLY)". When OIDC is not configured, the toggle is disabled. - **Invalidation:** Enabling OIDC-only sets `registration_enabled: false` in one update; disabling OIDC-only only updates `oidc_only` (registration left as-is). - **Review focus:** Plug order in router (OidcOnlySignInRedirect), policy/validation order in User, and that all OIDC-only paths (form, plug, controller) stay consistent. Reviewed-on: #474 Co-authored-by: Simon <s.thiessen@local-it.org> Co-committed-by: Simon <s.thiessen@local-it.org>
237 lines
7.7 KiB
Elixir
237 lines
7.7 KiB
Elixir
defmodule MvWeb.Router do
|
|
use MvWeb, :router
|
|
|
|
use AshAuthentication.Phoenix.Router
|
|
|
|
import AshAuthentication.Plug.Helpers
|
|
|
|
pipeline :browser do
|
|
plug :accepts, ["html"]
|
|
plug :fetch_session
|
|
plug :fetch_live_flash
|
|
plug :put_root_layout, html: {MvWeb.Layouts, :root}
|
|
plug :protect_from_forgery
|
|
plug :put_secure_browser_headers
|
|
plug :load_from_session
|
|
plug :set_locale
|
|
plug MvWeb.Plugs.AssignClubName
|
|
plug MvWeb.Plugs.CheckPagePermission
|
|
plug MvWeb.Plugs.JoinFormEnabled
|
|
plug MvWeb.Plugs.RegistrationEnabled
|
|
plug MvWeb.Plugs.OidcOnlySignInRedirect
|
|
end
|
|
|
|
pipeline :api do
|
|
plug :accepts, ["json"]
|
|
plug :load_from_bearer
|
|
plug :set_actor, :user
|
|
end
|
|
|
|
scope "/", MvWeb do
|
|
pipe_through :browser
|
|
|
|
ash_authentication_live_session :authenticated_routes do
|
|
# in each liveview, add one of the following at the top of the module:
|
|
#
|
|
# If an authenticated user must be present:
|
|
# on_mount {MvWeb.LiveUserAuth, :live_user_required}
|
|
#
|
|
# If an authenticated user *may* be present:
|
|
# on_mount {MvWeb.LiveUserAuth, :live_user_optional}
|
|
#
|
|
# If an authenticated user must *not* be present:
|
|
# on_mount {MvWeb.LiveUserAuth, :live_no_user}
|
|
end
|
|
end
|
|
|
|
scope "/", MvWeb do
|
|
pipe_through :browser
|
|
|
|
@doc """
|
|
AshAuthentication-specific: We define that all routes can only be accessed when the user is signed in.
|
|
"""
|
|
ash_authentication_live_session :authentication_required,
|
|
on_mount: [
|
|
{MvWeb.LiveUserAuth, :live_user_required},
|
|
{MvWeb.LiveHelpers, :ensure_user_role_loaded},
|
|
{MvWeb.LiveHelpers, :check_page_permission_on_params}
|
|
] do
|
|
live "/", MemberLive.Index, :index
|
|
|
|
live "/members", MemberLive.Index, :index
|
|
live "/members/new", MemberLive.Form, :new
|
|
live "/members/:id/edit", MemberLive.Form, :edit
|
|
live "/members/:id", MemberLive.Show, :show
|
|
live "/members/:id/show/edit", MemberLive.Show, :edit
|
|
|
|
live "/users", UserLive.Index, :index
|
|
live "/users/new", UserLive.Form, :new
|
|
live "/users/:id/edit", UserLive.Form, :edit
|
|
live "/users/:id", UserLive.Show, :show
|
|
live "/users/:id/show/edit", UserLive.Show, :edit
|
|
|
|
live "/settings", GlobalSettingsLive
|
|
|
|
# Membership Fee Settings (includes fee types list; new/edit under sub-routes)
|
|
live "/membership_fee_settings", MembershipFeeSettingsLive
|
|
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
|
|
|
|
# Groups Management
|
|
live "/groups", GroupLive.Index, :index
|
|
live "/groups/new", GroupLive.Form, :new
|
|
live "/groups/:slug", GroupLive.Show, :show
|
|
live "/groups/:slug/edit", GroupLive.Form, :edit
|
|
|
|
# Join Request Approval (normal_user and admin)
|
|
live "/join_requests", JoinRequestLive.Index, :index
|
|
live "/join_requests/:id", JoinRequestLive.Show, :show
|
|
|
|
# Role Management (Admin only)
|
|
live "/admin/roles", RoleLive.Index, :index
|
|
live "/admin/roles/new", RoleLive.Form, :new
|
|
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
|
|
|
|
post "/members/export.csv", MemberExportController, :export
|
|
post "/members/export.pdf", MemberPdfExportController, :export
|
|
post "/set_locale", LocaleController, :set_locale
|
|
end
|
|
|
|
# OIDC account linking - user needs to verify password (MUST be before auth_routes!)
|
|
live "/auth/link-oidc-account", LinkOidcAccountLive
|
|
|
|
# ASHAUTHENTICATION GENERATED AUTH ROUTES
|
|
auth_routes AuthController, Mv.Accounts.User, path: "/auth"
|
|
sign_out_route AuthController
|
|
|
|
# Remove these if you'd like to use your own authentication views
|
|
sign_in_route register_path: "/register",
|
|
reset_path: "/reset",
|
|
auth_routes_prefix: "/auth",
|
|
on_mount: [{MvWeb.LiveUserAuth, :live_no_user}],
|
|
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
|
|
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",
|
|
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
|
|
gettext_backend: {MvWeb.Gettext, "auth"}
|
|
|
|
# Remove this if you do not use the confirmation strategy
|
|
confirm_route Mv.Accounts.User, :confirm_new_user,
|
|
auth_routes_prefix: "/auth",
|
|
overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.DaisyUI],
|
|
gettext_backend: {MvWeb.Gettext, "auth"}
|
|
|
|
# Public join page (no auth required)
|
|
live_session :public_join,
|
|
on_mount: [{MvWeb.LiveUserAuth, :live_user_optional}] do
|
|
live "/join", JoinLive, :index
|
|
end
|
|
|
|
# Public join confirmation (double opt-in); /confirm* is already public in CheckPagePermission
|
|
get "/confirm_join/:token", JoinConfirmController, :confirm
|
|
|
|
# Remove this if you do not use the magic link strategy.
|
|
# magic_sign_in_route(Mv.Accounts.User, :magic_link,
|
|
# auth_routes_prefix: "/auth",
|
|
# overrides: [MvWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default]
|
|
# )
|
|
end
|
|
|
|
# Other scopes may use custom stacks.
|
|
# scope "/api", MvWeb do
|
|
# pipe_through :api
|
|
# end
|
|
|
|
# Enable LiveDashboard and Swoosh mailbox preview in development
|
|
if Application.compile_env(:mv, :dev_routes) do
|
|
# If you want to use the LiveDashboard in production, you should put
|
|
# it behind authentication and allow only admins to access it.
|
|
# If your application does not have an admins-only section yet,
|
|
# you can use Plug.BasicAuth to set up some basic authentication
|
|
# as long as you are also using SSL (which you should anyway).
|
|
import Phoenix.LiveDashboard.Router
|
|
|
|
scope "/dev" do
|
|
pipe_through :browser
|
|
|
|
live_dashboard "/dashboard", metrics: MvWeb.Telemetry
|
|
forward "/mailbox", Plug.Swoosh.MailboxPreview
|
|
end
|
|
end
|
|
|
|
if Application.compile_env(:mv, :dev_routes) do
|
|
import AshAdmin.Router
|
|
|
|
scope "/admin" do
|
|
pipe_through :browser
|
|
|
|
ash_admin "/"
|
|
end
|
|
end
|
|
|
|
defp set_locale(conn, _opts) do
|
|
locale =
|
|
get_session(conn, :locale) ||
|
|
get_locale_from_cookie(conn) ||
|
|
extract_locale_from_headers(conn.req_headers)
|
|
|
|
Gettext.put_locale(MvWeb.Gettext, locale)
|
|
|
|
conn
|
|
|> put_session(:locale, locale)
|
|
|> assign(:locale, locale)
|
|
end
|
|
|
|
defp get_locale_from_cookie(conn) do
|
|
case conn.req_cookies do
|
|
%{"locale" => locale} when locale in ["en", "de"] -> locale
|
|
_ -> nil
|
|
end
|
|
end
|
|
|
|
# Get locale from user
|
|
defp extract_locale_from_headers(headers) do
|
|
headers
|
|
|> Enum.find_value(fn
|
|
{"accept-language", value} -> value
|
|
_ -> nil
|
|
end)
|
|
|> parse_accept_language()
|
|
|> Enum.find(&supported_locale?/1)
|
|
|> fallback_locale()
|
|
end
|
|
|
|
defp parse_accept_language(nil), do: []
|
|
|
|
defp parse_accept_language(header) do
|
|
header
|
|
|> String.split(",")
|
|
|> Enum.map(&String.trim/1)
|
|
|> Enum.map(fn lang ->
|
|
lang
|
|
# we only want the first part
|
|
|> String.split(";")
|
|
|> hd()
|
|
|> String.split("-")
|
|
|> hd()
|
|
end)
|
|
end
|
|
|
|
# Our supported languages: German and English; default German.
|
|
defp supported_locale?(locale), do: locale in ["en", "de"]
|
|
defp fallback_locale(nil), do: Application.get_env(:mv, :default_locale, "de")
|
|
defp fallback_locale(locale), do: locale
|
|
end
|