split RMS login email into callsign and domain select

This commit is contained in:
2026-04-02 22:42:49 +02:00
parent bc2f972769
commit 8c031a939c
2 changed files with 89 additions and 10 deletions

View File

@@ -81,7 +81,8 @@ const state = {
const els = { const els = {
authForm: document.getElementById("authForm"), authForm: document.getElementById("authForm"),
email: document.getElementById("email"), callsignInput: document.getElementById("callsignInput"),
emailDomainSelect: document.getElementById("emailDomainSelect"),
loginBtn: document.getElementById("loginBtn"), loginBtn: document.getElementById("loginBtn"),
authMethodSelect: document.getElementById("authMethodSelect"), authMethodSelect: document.getElementById("authMethodSelect"),
otpWrap: document.getElementById("otpWrap"), otpWrap: document.getElementById("otpWrap"),
@@ -910,7 +911,7 @@ async function refreshFrontendOnResume() {
async function requestAccess() { async function requestAccess() {
clearMessages("auth"); clearMessages("auth");
const email = els.email.value.trim(); const email = composeLoginEmail();
if (!isLoginEmailAllowed(email)) { if (!isLoginEmailAllowed(email)) {
renderMessage(els.authMessage, `Nur Club-Mailadressen (${formatAllowedDomainsHint()}) sind zum Anmelden moeglich.`, true); renderMessage(els.authMessage, `Nur Club-Mailadressen (${formatAllowedDomainsHint()}) sind zum Anmelden moeglich.`, true);
return; return;
@@ -950,11 +951,12 @@ async function requestAccess() {
async function verifyOtpCode() { async function verifyOtpCode() {
clearMessages("auth"); clearMessages("auth");
const email = composeLoginEmail();
try { try {
const result = await api("/v1/auth/verify-email", { const result = await api("/v1/auth/verify-email", {
method: "POST", method: "POST",
body: { body: {
email: els.email.value.trim(), email,
code: els.otpCode.value.trim() code: els.otpCode.value.trim()
}, },
authRequired: false authRequired: false
@@ -997,6 +999,41 @@ function isLoginEmailAllowed(email) {
return getAllowedLoginDomains().includes(domain); return getAllowedLoginDomains().includes(domain);
} }
function composeLoginEmail() {
const callsign = String(els.callsignInput && els.callsignInput.value ? els.callsignInput.value : "")
.trim()
.toLowerCase();
const selectedDomain = String(
els.emailDomainSelect && els.emailDomainSelect.value
? els.emailDomainSelect.value
: getAllowedLoginDomains()[0] || ""
)
.trim()
.toLowerCase();
if (!callsign || !selectedDomain) {
return "";
}
if (callsign.includes("@")) {
return callsign;
}
return `${callsign}@${selectedDomain}`;
}
function splitEmailParts(email) {
const normalized = String(email || "").trim().toLowerCase();
const atIndex = normalized.lastIndexOf("@");
if (atIndex < 0) {
return {
callsign: normalized,
domain: ""
};
}
return {
callsign: normalized.slice(0, atIndex),
domain: normalized.slice(atIndex + 1)
};
}
function getAllowedLoginDomains() { function getAllowedLoginDomains() {
const list = state && state.system && Array.isArray(state.system.allowedLoginDomains) const list = state && state.system && Array.isArray(state.system.allowedLoginDomains)
? state.system.allowedLoginDomains ? state.system.allowedLoginDomains
@@ -1008,12 +1045,32 @@ function formatAllowedDomainsHint() {
return getAllowedLoginDomains().map((domain) => `@${domain}`).join(" oder "); return getAllowedLoginDomains().map((domain) => `@${domain}`).join(" oder ");
} }
function renderLoginDomainSelect() {
if (!els.emailDomainSelect) {
return;
}
const domains = getAllowedLoginDomains();
const previous = String(els.emailDomainSelect.value || "").toLowerCase();
els.emailDomainSelect.innerHTML = "";
for (const domain of domains) {
const option = document.createElement("option");
option.value = domain;
option.textContent = domain;
els.emailDomainSelect.appendChild(option);
}
if (previous && domains.includes(previous)) {
els.emailDomainSelect.value = previous;
} else if (domains.length > 0) {
els.emailDomainSelect.value = domains[0];
}
}
async function handleEmailTokenFromUrl() { async function handleEmailTokenFromUrl() {
const url = new URL(window.location.href); const url = new URL(window.location.href);
if (url.searchParams.get("requestApproval") === "1") { if (url.searchParams.get("requestApproval") === "1") {
const email = (url.searchParams.get("email") || "").trim(); const email = (url.searchParams.get("email") || "").trim();
if (email) { if (email) {
els.email.value = email; setLoginEmail(email);
try { try {
const result = await api("/v1/auth/request-approval", { const result = await api("/v1/auth/request-approval", {
method: "POST", method: "POST",
@@ -1066,6 +1123,19 @@ async function handleEmailTokenFromUrl() {
} }
} }
function setLoginEmail(email) {
const parts = splitEmailParts(email);
if (els.callsignInput) {
els.callsignInput.value = parts.callsign || "";
}
if (els.emailDomainSelect) {
renderLoginDomainSelect();
if (parts.domain && getAllowedLoginDomains().includes(parts.domain)) {
els.emailDomainSelect.value = parts.domain;
}
}
}
async function refreshPublicSystemStatus() { async function refreshPublicSystemStatus() {
try { try {
const result = await api("/v1/public/system", { authRequired: false }); const result = await api("/v1/public/system", { authRequired: false });
@@ -1085,6 +1155,7 @@ async function refreshPublicSystemStatus() {
allowedLoginDomains: DEFAULT_ALLOWED_LOGIN_DOMAINS.slice() allowedLoginDomains: DEFAULT_ALLOWED_LOGIN_DOMAINS.slice()
}; };
} }
renderLoginDomainSelect();
renderMaintenanceBanner(); renderMaintenanceBanner();
renderBranding(); renderBranding();
} }
@@ -4225,7 +4296,8 @@ function updateUserUi() {
setDisabled(els.logoutBtn, !loggedIn); setDisabled(els.logoutBtn, !loggedIn);
setDisabled(els.settingsLogoutTopBtn, !loggedIn); setDisabled(els.settingsLogoutTopBtn, !loggedIn);
setDisabled(els.userMenuButton, !loggedIn); setDisabled(els.userMenuButton, !loggedIn);
setDisabled(els.email, loggedIn); setDisabled(els.callsignInput, loggedIn);
setDisabled(els.emailDomainSelect, loggedIn);
if (els.userMenuButton) { if (els.userMenuButton) {
els.userMenuButton.textContent = "☰"; els.userMenuButton.textContent = "☰";
els.userMenuButton.setAttribute("aria-label", loggedIn ? `Menue (${state.user.email})` : "Menue"); els.userMenuButton.setAttribute("aria-label", loggedIn ? `Menue (${state.user.email})` : "Menue");

View File

@@ -59,14 +59,21 @@
<section class="view auth-view" id="authView"> <section class="view auth-view" id="authView">
<article class="card login-card stagger" id="authCard"> <article class="card login-card stagger" id="authCard">
<h2>Anmeldung</h2> <h2>Anmeldung</h2>
<p class="muted">Anmeldung per ARCG E-Mail-Adresse wird automatisch freigeschalten. Andere Mailadressen brauchen eine manuelle Freischaltung. Du bekommst einen Login- oder Bestaetigungslink.</p> <p class="muted">Bitte Rufzeichen und Club-Domain eingeben. Du bekommst einen Login- oder Bestaetigungslink.</p>
<p id="maintenanceBanner" class="message" hidden></p> <p id="maintenanceBanner" class="message" hidden></p>
<form id="authForm" class="stack"> <form id="authForm" class="stack">
<label class="field"> <div class="actions" style="align-items: end; gap: 0.6rem; flex-wrap: wrap;">
<span>E-Mail</span> <label class="field" style="flex: 2 1 220px; min-width: 180px; margin: 0;">
<input id="email" type="email" autocomplete="email" required /> <span>Rufzeichen</span>
<input id="callsignInput" type="text" autocomplete="username" placeholder="oe6abc" required />
</label> </label>
<span class="muted" aria-hidden="true" style="padding-bottom: 0.5rem;">@</span>
<label class="field" style="flex: 1 1 180px; min-width: 160px; margin: 0;">
<span>Domain</span>
<select id="emailDomainSelect"></select>
</label>
</div>
<label class="field"> <label class="field">
<span>Bestaetigungsart</span> <span>Bestaetigungsart</span>
<select id="authMethodSelect"></select> <select id="authMethodSelect"></select>