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>
112 lines
4.1 KiB
Text
112 lines
4.1 KiB
Text
<!DOCTYPE html>
|
|
<html lang={Gettext.get_locale()}>
|
|
<head>
|
|
{Application.get_env(:live_debugger, :live_debugger_tags)}
|
|
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<meta name="csrf-token" content={get_csrf_token()} />
|
|
<link phx-track-static rel="icon" type="image/svg+xml" href={~p"/images/mila.svg"} />
|
|
<.live_title default="Mila">
|
|
{page_title_string(assigns)}
|
|
</.live_title>
|
|
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} />
|
|
<script defer phx-track-static type="text/javascript" src={~p"/assets/js/app.js"}>
|
|
</script>
|
|
<script>
|
|
(() => {
|
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const systemTheme = () => (mq.matches ? "dark" : "light");
|
|
|
|
// Single source of truth:
|
|
// - localStorage["phx:theme"] = "light" | "dark" (explicit override)
|
|
// - missing key => "system"
|
|
const storedTheme = () => localStorage.getItem("phx:theme") || "system";
|
|
|
|
const effectiveTheme = (t) => (t === "system" ? systemTheme() : t);
|
|
|
|
const applyThemeNow = (t) => {
|
|
document.documentElement.setAttribute("data-theme", effectiveTheme(t));
|
|
};
|
|
|
|
const syncToggle = () => {
|
|
const eff = effectiveTheme(storedTheme());
|
|
document.querySelectorAll("[data-theme-toggle]").forEach((el) => {
|
|
el.checked = eff === "dark";
|
|
});
|
|
};
|
|
|
|
const setTheme = (t) => {
|
|
if (t === "system") localStorage.removeItem("phx:theme");
|
|
else localStorage.setItem("phx:theme", t);
|
|
|
|
applyThemeNow(t);
|
|
syncToggle(); // if toggle exists already
|
|
};
|
|
|
|
// 1) Apply theme ASAP to match system on first paint
|
|
applyThemeNow(storedTheme());
|
|
|
|
// 2) Sync toggle once DOM is ready (fixes initial "light" toggle)
|
|
document.addEventListener("DOMContentLoaded", syncToggle);
|
|
|
|
// 3) If toggle appears later (LiveView render), sync immediately
|
|
const obs = new MutationObserver(() => {
|
|
if (document.querySelector("[data-theme-toggle]")) syncToggle();
|
|
});
|
|
obs.observe(document.documentElement, { childList: true, subtree: true });
|
|
|
|
window.addEventListener("phx:set-theme", ({ detail: { theme } }) => setTheme(theme));
|
|
window.addEventListener("storage", (e) => e.key === "phx:theme" && setTheme(e.newValue || "system"));
|
|
|
|
mq.addEventListener("change", () => {
|
|
if (localStorage.getItem("phx:theme") === null) {
|
|
applyThemeNow("system");
|
|
syncToggle();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div
|
|
id="flash-group-root"
|
|
aria-live="polite"
|
|
class="z-50 flex flex-col gap-2 toast toast-bottom toast-end"
|
|
>
|
|
<.flash id="flash-success-root" kind={:success} flash={@flash} auto_clear_ms={5000} />
|
|
<.flash id="flash-warning-root" kind={:warning} flash={@flash} />
|
|
<.flash id="flash-info-root" kind={:info} flash={@flash} />
|
|
<.flash id="flash-error-root" kind={:error} flash={@flash} />
|
|
|
|
<.flash
|
|
id="client-error-root"
|
|
kind={:error}
|
|
title={gettext("We can't find the internet")}
|
|
phx-disconnected={
|
|
show(".phx-client-error #client-error-root") |> JS.remove_attribute("hidden")
|
|
}
|
|
phx-connected={hide("#client-error-root") |> JS.set_attribute({"hidden", ""})}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
|
|
<.flash
|
|
id="server-error-root"
|
|
kind={:error}
|
|
title={gettext("Something went wrong!")}
|
|
phx-disconnected={
|
|
show(".phx-server-error #server-error-root") |> JS.remove_attribute("hidden")
|
|
}
|
|
phx-connected={hide("#server-error-root") |> JS.set_attribute({"hidden", ""})}
|
|
hidden
|
|
>
|
|
{gettext("Attempting to reconnect")}
|
|
<.icon name="hero-arrow-path" class="ml-1 size-3 motion-safe:animate-spin" />
|
|
</.flash>
|
|
</div>
|
|
{@inner_content}
|
|
</body>
|
|
</html>
|