/**
* UltiSuite — enrichissements DOM des flows Authentik (shadow roots inclus).
* Actuellement : ulti-enrollment (suffixe e-mail, toggle mot de passe, robustesse).
*/
(function () {
"use strict";
const ENROLLMENT_SLUG = "ulti-enrollment";
const DEFAULT_MAIL_SUFFIX = "@ultisuite.fr";
const EMAIL_CHECK_DEBOUNCE_MS = 1000;
const MIN_EMAIL_LOCAL_LENGTH = 2;
const EYE_SVG =
'';
const EYE_OFF_SVG =
'';
function forEachDeep(root, visit) {
if (!root) return;
visit(root);
const nodes = root.querySelectorAll ? root.querySelectorAll("*") : [];
for (const node of nodes) {
if (node.shadowRoot) forEachDeep(node.shadowRoot, visit);
}
}
function flowSlug(executor) {
return (
executor.getAttribute("flowslug") ||
executor.getAttribute("flowSlug") ||
""
);
}
function mailSuffixFromForm(form) {
const hidden = form.querySelector('input[name="email"]');
const raw = hidden?.value || hidden?.getAttribute("value") || "";
const at = raw.indexOf("@");
if (at >= 0) return raw.slice(at);
return DEFAULT_MAIL_SUFFIX;
}
function mailDomainFromForm(form) {
const suffix = mailSuffixFromForm(form);
return suffix.replace(/^@+/, "");
}
function normalizeSignupLocalPart(raw) {
return String(raw || "")
.replace(/@+/g, "")
.trim()
.toLowerCase();
}
function evaluatePasswordStrength(password) {
if (!password) return { level: "empty", label: "" };
let score = 0;
if (password.length >= 8) score += 1;
if (password.length >= 12) score += 1;
if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score += 1;
if (/\d/.test(password)) score += 1;
if (/[^A-Za-z0-9]/.test(password)) score += 1;
if (score <= 1) return { level: "weak", label: "Faible" };
if (score === 2) return { level: "fair", label: "Moyen" };
if (score === 3 || score === 4) return { level: "good", label: "Bon" };
return { level: "strong", label: "Fort" };
}
function activeSegments(level) {
if (level === "weak") return 1;
if (level === "fair") return 2;
if (level === "good") return 3;
if (level === "strong") return 4;
return 0;
}
function createStrengthMeter(input) {
const wrap = document.createElement("div");
wrap.className = "ulti-password-strength";
wrap.hidden = true;
wrap.setAttribute("aria-live", "polite");
const bars = document.createElement("div");
bars.className = "ulti-password-strength-bars";
for (let i = 0; i < 4; i += 1) {
bars.appendChild(document.createElement("span"));
}
const label = document.createElement("p");
label.className = "ulti-password-strength-label";
label.innerHTML = 'Robustesse : ';
wrap.appendChild(bars);
wrap.appendChild(label);
const update = () => {
const { level, label: text } = evaluatePasswordStrength(input.value);
wrap.hidden = !input.value;
const segs = activeSegments(level);
bars.querySelectorAll("span").forEach((bar, index) => {
bar.className = index < segs ? `active ${level}` : "";
});
wrap.querySelector(".ulti-password-strength-value").textContent = text;
};
input.addEventListener("input", update);
update();
return wrap;
}
function createMatchIndicator(primary, repeat) {
const el = document.createElement("p");
el.className = "ulti-password-match";
el.hidden = true;
el.setAttribute("role", "status");
const update = () => {
const a = primary.value;
const b = repeat.value;
if (!b) {
el.hidden = true;
return;
}
el.hidden = false;
const ok = a === b;
el.className = ok ? "ulti-password-match ok" : "ulti-password-match bad";
el.textContent = ok
? "Les mots de passe correspondent."
: "Les mots de passe ne correspondent pas.";
};
primary.addEventListener("input", update);
repeat.addEventListener("input", update);
update();
return el;
}
function wrapPasswordInput(input) {
if (input.closest(".ulti-password-wrap")) return input.closest(".ulti-password-wrap");
const wrap = document.createElement("div");
wrap.className = "ulti-password-wrap";
input.parentNode.insertBefore(wrap, input);
wrap.appendChild(input);
input.classList.add("ulti-password-input");
const btn = document.createElement("button");
btn.type = "button";
btn.className = "ulti-password-toggle";
btn.setAttribute("aria-label", "Afficher le mot de passe");
btn.innerHTML = EYE_SVG;
btn.addEventListener("click", () => {
const visible = input.type === "text";
input.type = visible ? "password" : "text";
btn.setAttribute(
"aria-label",
visible ? "Afficher le mot de passe" : "Masquer le mot de passe"
);
btn.innerHTML = visible ? EYE_SVG : EYE_OFF_SVG;
});
wrap.appendChild(btn);
return wrap;
}
function enhanceEmailField(form) {
const input = form.querySelector('input[name="username"]');
if (!input || input.closest(".ulti-email-input-group")) return;
form.querySelectorAll("p").forEach((p) => {
if (p.textContent.trim().startsWith("@")) p.hidden = true;
});
const group = document.createElement("div");
group.className = "ulti-email-input-group";
input.parentNode.insertBefore(group, input);
group.appendChild(input);
const suffix = document.createElement("span");
suffix.className = "ulti-email-suffix";
suffix.textContent = mailSuffixFromForm(form);
group.appendChild(suffix);
attachEmailAvailabilityChecker(form, input);
}
function attachEmailAvailabilityChecker(form, input) {
if (input.dataset.ultiAvailability === "1") return;
input.dataset.ultiAvailability = "1";
const hint = document.createElement("p");
hint.className = "ulti-email-availability";
hint.hidden = true;
hint.setAttribute("aria-live", "polite");
const host = input.closest("ak-form-element");
if (host) host.after(hint);
let debounceTimer = null;
let requestId = 0;
let debouncedLocal = "";
let status = "idle";
function submitButton() {
return form.querySelector(
'.pf-c-form__group.pf-m-action button[type="submit"]'
);
}
function setSubmitDisabled(disabled) {
const btn = submitButton();
if (!btn) return;
btn.disabled = disabled;
btn.setAttribute("aria-disabled", disabled ? "true" : "false");
}
function render(local) {
if (local.length === 0) {
hint.hidden = true;
input.removeAttribute("aria-invalid");
setSubmitDisabled(false);
return;
}
hint.hidden = false;
hint.className = "ulti-email-availability";
if (local.length < MIN_EMAIL_LOCAL_LENGTH) {
hint.classList.add("muted");
hint.textContent = "Au moins 2 caractères avant le @.";
input.removeAttribute("aria-invalid");
setSubmitDisabled(true);
return;
}
if (local !== debouncedLocal || status === "checking") {
hint.classList.add("checking");
hint.textContent = "Vérification de la disponibilité…";
input.removeAttribute("aria-invalid");
setSubmitDisabled(true);
return;
}
if (status === "available") {
hint.classList.add("ok");
hint.setAttribute("role", "status");
hint.textContent = "Cette adresse est disponible.";
input.removeAttribute("aria-invalid");
setSubmitDisabled(false);
return;
}
if (status === "taken") {
hint.classList.add("bad");
hint.setAttribute("role", "alert");
hint.textContent = "Cette adresse est déjà prise.";
input.setAttribute("aria-invalid", "true");
setSubmitDisabled(true);
return;
}
if (status === "error") {
hint.classList.add("muted");
hint.textContent =
"Impossible de vérifier la disponibilité pour le moment.";
input.removeAttribute("aria-invalid");
setSubmitDisabled(false);
return;
}
hint.hidden = true;
setSubmitDisabled(false);
}
async function runCheck(local) {
const domain = mailDomainFromForm(form);
const id = ++requestId;
status = "checking";
render(local);
try {
const params = new URLSearchParams({ local, domain });
const res = await fetch(
`/api/v1/mail/addresses/check?${params.toString()}`,
{
headers: { Accept: "application/json" },
credentials: "include",
}
);
if (id !== requestId) return;
if (!res.ok) throw new Error(`address check failed (${res.status})`);
const data = await res.json();
status = data.available ? "available" : "taken";
} catch {
if (id !== requestId) return;
status = "error";
}
render(normalizeSignupLocalPart(input.value));
}
function scheduleCheck() {
const local = normalizeSignupLocalPart(input.value);
window.clearTimeout(debounceTimer);
if (local.length < MIN_EMAIL_LOCAL_LENGTH) {
requestId += 1;
debouncedLocal = local;
status = "idle";
render(local);
return;
}
status = "checking";
render(local);
debounceTimer = window.setTimeout(() => {
debouncedLocal = local;
void runCheck(local);
}, EMAIL_CHECK_DEBOUNCE_MS);
}
input.addEventListener("input", scheduleCheck);
input.addEventListener("keydown", (event) => {
if (event.key === "@") event.preventDefault();
});
input.addEventListener("paste", (event) => {
const text = event.clipboardData?.getData("text") ?? "";
if (!text.includes("@")) return;
event.preventDefault();
input.value = normalizeSignupLocalPart(text);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
scheduleCheck();
}
function enhanceEnrollmentForm(form) {
if (form.dataset.ultiEnhanced === "1") return;
if (!form.querySelector('input[name="username"]')) return;
form.dataset.ultiEnhanced = "1";
enhanceEmailField(form);
const password = form.querySelector('input[name="password"]');
const repeat = form.querySelector('input[name="password_repeat"]');
if (!password || !repeat) return;
wrapPasswordInput(password);
wrapPasswordInput(repeat);
const passwordHost = password.closest("ak-form-element");
if (passwordHost && !form.querySelector(".ulti-password-strength")) {
passwordHost.after(createStrengthMeter(password));
}
const repeatHost = repeat.closest("ak-form-element");
if (repeatHost && !form.querySelector(".ulti-password-match")) {
repeatHost.after(createMatchIndicator(password, repeat));
}
}
function scan() {
const executor = document.querySelector("ak-flow-executor");
if (!executor || flowSlug(executor) !== ENROLLMENT_SLUG) return false;
let found = false;
if (executor.shadowRoot) {
forEachDeep(executor.shadowRoot, (root) => {
root.querySelectorAll?.("form.pf-c-form")?.forEach((form) => {
found = true;
enhanceEnrollmentForm(form);
});
});
}
return found;
}
function observeExecutor(executor) {
if (!executor?.shadowRoot || executor.dataset.ultiObserved === "1") return;
executor.dataset.ultiObserved = "1";
new MutationObserver(() => scan()).observe(executor.shadowRoot, {
childList: true,
subtree: true,
});
}
function boot() {
let tries = 0;
const tick = () => {
tries += 1;
const executor = document.querySelector("ak-flow-executor");
observeExecutor(executor);
scan();
const enhanced = (() => {
const executor = document.querySelector("ak-flow-executor");
if (!executor?.shadowRoot) return null;
let match = null;
forEachDeep(executor.shadowRoot, (root) => {
match =
match ||
root.querySelector?.('form.pf-c-form[data-ulti-enhanced="1"]') ||
null;
});
return match;
})();
if (!enhanced && tries < 80) {
window.setTimeout(tick, 150);
}
};
tick();
new MutationObserver(() => {
observeExecutor(document.querySelector("ak-flow-executor"));
scan();
}).observe(document.body, { childList: true, subtree: true });
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot);
} else {
boot();
}
})();