浏览代码

Merge pull request #6521 from mailcow/feat/login-quicklinks

[Web] Add quick links to other login pages and mailcow login toggle
FreddleSpl0it 3 月之前
父节点
当前提交
75d7f06b25

+ 2 - 1
data/web/admin/index.php

@@ -22,7 +22,8 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
 
 $template = 'admin_index.twig';
 $template_data = [
-  'login_delay' => @$_SESSION['ldelay']
+  'login_delay' => @$_SESSION['ldelay'],
+  'custom_login' => customize('get', 'custom_login'),
 ];
 
 $js_minifier->add('/web/js/site/index.js');

+ 1 - 0
data/web/admin/system.php

@@ -125,6 +125,7 @@ $template_data = [
   'logo_specs' => customize('get', 'main_logo_specs'),
   'logo_dark_specs' => customize('get', 'main_logo_dark_specs'),
   'ip_check' => customize('get', 'ip_check'),
+  'custom_login' => customize('get', 'custom_login'),
   'password_complexity' => password_complexity('get'),
   'show_rspamd_global_filters' => @$_SESSION['show_rspamd_global_filters'],
   'cors_settings' => $cors_settings,

+ 1 - 0
data/web/domainadmin/index.php

@@ -22,6 +22,7 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
 $template = 'domainadmin_index.twig';
 $template_data = [
   'login_delay' => @$_SESSION['ldelay'],
+  'custom_login' => customize('get', 'custom_login'),
 ];
 
 $js_minifier->add('/web/js/site/index.js');

+ 43 - 0
data/web/inc/functions.customize.inc.php

@@ -204,6 +204,35 @@ function customize($_action, $_item, $_data = null) {
             'msg' => 'ip_check_opt_in_modified'
           );
         break;
+        case 'custom_login':
+          $hide_user_quicklink        = ($_data['hide_user_quicklink'] == "1") ? 1 : 0;
+          $hide_domainadmin_quicklink = ($_data['hide_domainadmin_quicklink'] == "1") ? 1 : 0;
+          $hide_admin_quicklink       = ($_data['hide_admin_quicklink'] == "1") ? 1 : 0;
+          $force_sso                  = ($_data['force_sso'] == "1") ? 1 : 0;
+
+          $custom_login = array(
+            "hide_user_quicklink" => $hide_user_quicklink,
+            "hide_domainadmin_quicklink" => $hide_domainadmin_quicklink,
+            "hide_admin_quicklink" => $hide_admin_quicklink,
+            "force_sso" => $force_sso,
+          );
+          try {
+            $redis->set('CUSTOM_LOGIN', json_encode($custom_login));
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_item, $_data),
+              'msg' => array('redis_error', $e)
+            );
+            return false;
+          }
+          $_SESSION['return'][] = array(
+            'type' => 'success',
+            'log' => array(__FUNCTION__, $_action, $_item, $_data),
+            'msg' => 'custom_login_modified'
+          );
+        break;
       }
     break;
     case 'delete':
@@ -357,6 +386,20 @@ function customize($_action, $_item, $_data = null) {
             return false;
           }
         break;
+        case 'custom_login':
+          try {
+            $custom_login = ($custom_login = $redis->get('CUSTOM_LOGIN')) ? $custom_login : array();
+            return json_decode($custom_login, true);
+          }
+          catch (RedisException $e) {
+            $_SESSION['return'][] = array(
+              'type' => 'danger',
+              'log' => array(__FUNCTION__, $_action, $_item, $_data),
+              'msg' => array('redis_error', $e)
+            );
+            return false;
+          }
+        break;
       }
     break;
   }

+ 5 - 3
data/web/index.php

@@ -33,16 +33,18 @@ $_SESSION['index_query_string'] = $_SERVER['QUERY_STRING'];
 
 $has_iam_sso = false;
 if ($iam_provider){
-  $has_iam_sso = identity_provider("get-redirect") ? true : false;
+  $iam_redirect_url = identity_provider("get-redirect");
+  $has_iam_sso = $iam_redirect_url ? true : false;
 }
-
+$custom_login = customize('get', 'custom_login');
 
 $template = 'user_index.twig';
 $template_data = [
   'oauth2_request' => @$_SESSION['oauth2_request'],
   'is_mobileconfig' => str_contains($_SESSION['index_query_string'], 'mobileconfig'),
   'login_delay' => @$_SESSION['ldelay'],
-  'has_iam_sso' => $has_iam_sso
+  'has_iam_sso' => $has_iam_sso,
+  'custom_login' => $custom_login,
 ];
 
 $js_minifier->add('/web/js/site/index.js');

+ 3 - 0
data/web/json_api.php

@@ -1976,6 +1976,9 @@ if (isset($_GET['query'])) {
         case "ip_check":
           process_edit_return(customize('edit', 'ip_check', $attr));
         break;
+        case "custom_login":
+          process_edit_return(customize('edit', 'custom_login', $attr));
+        break;
         case "self":
           if ($_SESSION['mailcow_cc_role'] == "domainadmin") {
             process_edit_return(domain_admin('edit', $attr));

+ 12 - 0
data/web/lang/lang.de-de.json

@@ -134,6 +134,7 @@
         "admin_domains": "Domain-Zuweisungen",
         "admins": "Administratoren",
         "admins_ldap": "LDAP-Administratoren",
+        "admin_quicklink": "Quicklink zur Admin-Loginseite ausblenden",
         "advanced_settings": "Erweiterte Einstellungen",
         "api_allow_from": "IP-Adressen oder Netzwerke (CIDR Notation) für Zugriff auf API",
         "api_info": "Die API befindet sich noch in Entwicklung, die Dokumentation kann unter <a href=\"/api\">/api</a> abgerufen werden.",
@@ -155,6 +156,7 @@
         "credentials_transport_warning": "<b>Warnung</b>: Das Hinzufügen einer neuen Regel bewirkt die Aktualisierung der Authentifizierungsdaten aller vorhandenen Einträge mit identischem Next Hop.",
         "customer_id": "Kunde",
         "customize": "UI-Anpassung",
+        "login_page": "Login-Seite",
         "destination": "Ziel",
         "dkim_add_key": "ARC/DKIM-Key hinzufügen",
         "dkim_domains_selector": "Selector",
@@ -173,6 +175,7 @@
         "domain": "Domain",
         "domain_admin": "Administrator hinzufügen",
         "domain_admins": "Domain-Administratoren",
+        "domainadmin_quicklink": "Quicklink zur Domainadmin-Loginseite ausblenden",
         "domain_s": "Domain(s)",
         "duplicate": "Duplizieren",
         "duplicate_dkim": "DKIM duplizieren",
@@ -195,6 +198,8 @@
         "f2b_retry_window": "Wiederholungen im Zeitraum von (s)",
         "f2b_whitelist": "Whitelist für Netzwerke und Hosts",
         "filter_table": "Tabelle filtern",
+        "force_sso_text": "Wenn ein externer OIDC-Provider konfiguriert ist, blendet diese Option die mailcow Loginform aus und zeigt nur den Single Sign-On-Button an.",
+        "force_sso": "mailcow Login deaktivieren und nur Single Sign-On anzeigen",
         "forwarding_hosts": "Weiterleitungs-Hosts",
         "forwarding_hosts_add_hint": "Sie können entweder IPv4-/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.",
         "forwarding_hosts_hint": "Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt, optional kann er aber in den Spam-Ordner einsortiert werden. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem mailcow-Server eingerichtet wurde.",
@@ -308,6 +313,7 @@
         "quarantine_release_format_att": "Als Anhang",
         "quarantine_release_format_raw": "Unverändertes Original",
         "quarantine_retention_size": "Rückhaltungen pro Mailbox:<br><small>0 bedeutet <b>inaktiv</b>.</small>",
+        "quicklink_text": "Quicklinks zu anderen Login-Seiten unter der Loginform ein- oder ausblenden",
         "quota_notification_html": "Benachrichtigungs-E-Mail Inhalt:<br><small>Leer lassen, um Standard-Template wiederherzustellen.</small>",
         "quota_notification_sender": "Benachrichtigungs-E-Mail Absender",
         "quota_notification_subject": "Benachrichtigungs-E-Mail Betreff",
@@ -387,6 +393,7 @@
         "unchanged_if_empty": "Unverändert, wenn leer",
         "upload": "Hochladen",
         "username": "Benutzername",
+        "user_quicklink": "Quicklink zur Benutzer-Loginseite ausblenden",
         "validate_license_now": "GUID erneut verifizieren",
         "verify": "Verifizieren",
         "yes": "&#10003;",
@@ -807,6 +814,10 @@
         "forgot_password": "> Passwort vergessen?",
         "invalid_pass_reset_token": "Der Rücksetz-Token für das Passwort ist ungültig oder abgelaufen.<br>Bitte fordern Sie einen neuen Link zur Passwortwiederherstellung an.",
         "login": "Anmelden",
+        "login_linkstext": "Nicht der richtige Login?",
+        "login_usertext": "Als Benutzer anmelden",
+        "login_domainadmintext": "Als Domainadmin anmelden",
+        "login_admintext": "Als Admin anmelden",
         "login_user": "Anmeldung als Benutzer",
         "login_dadmin": "Anmeldung als Domain-Administrator",
         "login_admin": "Anmeldung als Administrator",
@@ -1096,6 +1107,7 @@
         "bcc_edited": "BCC-Map-Eintrag %s wurde geändert",
         "bcc_saved": "BCC- Map-Eintrag wurde gespeichert",
         "cors_headers_edited": "CORS Einstellungen wurden erfolgreich gespeichert",
+        "custom_login_modified": "Login Anpassung wurde erfolgreich gespeichert",
         "db_init_complete": "Datenbankinitialisierung abgeschlossen",
         "delete_filter": "Filter-ID %s wurde gelöscht",
         "delete_filters": "Filter gelöscht: %s",

+ 12 - 0
data/web/lang/lang.en-gb.json

@@ -134,6 +134,7 @@
         "admin_domains": "Domain assignments",
         "admins": "Administrators",
         "admins_ldap": "LDAP Administrators",
+        "admin_quicklink": "Hide Quicklink to Admin Login Page",
         "advanced_settings": "Advanced settings",
         "allowed_methods": "Access-Control-Allow-Methods",
         "allowed_origins": "Access-Control-Allow-Origin",
@@ -161,6 +162,7 @@
         "credentials_transport_warning": "<b>Warning</b>: Adding a new transport map entry will update the credentials for all entries with a matching next hop column.",
         "customer_id": "Customer ID",
         "customize": "Customize",
+        "login_page": "Login Page",
         "destination": "Destination",
         "dkim_add_key": "Add ARC/DKIM key",
         "dkim_domains_selector": "Selector",
@@ -179,6 +181,7 @@
         "domain": "Domain",
         "domain_admin": "Domain administrator",
         "domain_admins": "Domain administrators",
+        "domainadmin_quicklink": "Hide Quicklink to Domainadmin Login Page",
         "domain_s": "Domain/s",
         "duplicate": "Duplicate",
         "duplicate_dkim": "Duplicate DKIM record",
@@ -202,6 +205,8 @@
         "f2b_whitelist": "Whitelisted networks/hosts",
         "filter": "Filter",
         "filter_table": "Filter table",
+        "force_sso_text": "If an external OIDC provider is configured, this option hides the default mailcow login froms and only shows the Singe Sign-On button",
+        "force_sso": "Disable mailcow Login and show only Singe Sign-On",
         "forwarding_hosts": "Forwarding Hosts",
         "forwarding_hosts_add_hint": "You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).",
         "forwarding_hosts_hint": "Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected, but optionally it can be filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your mailcow server.",
@@ -317,6 +322,7 @@
         "quarantine_release_format_att": "As attachment",
         "quarantine_release_format_raw": "Unmodified original",
         "quarantine_retention_size": "Retentions per mailbox:<br><small>0 indicates <b>inactive</b>.</small>",
+        "quicklink_text": "Show or hide quick links to other login pages under the login form",
         "quota_notification_html": "Notification email template:<br><small>Leave empty to restore default template.</small>",
         "quota_notification_sender": "Notification email sender",
         "quota_notification_subject": "Notification email subject",
@@ -398,6 +404,7 @@
         "upload": "Upload",
         "username": "Username",
         "user_link": "User-Link",
+        "user_quicklink": "Hide Quicklink to User Login Page",
         "validate_license_now": "Validate GUID against license server",
         "verify": "Verify",
         "yes": "&#10003;"
@@ -809,6 +816,10 @@
         "forgot_password": "> Forgot Password?",
         "invalid_pass_reset_token": "The reset password token is invalid or has expired.<br>Please request a new password reset link.",
         "login": "Login",
+        "login_linkstext": "Not the correct login?",
+        "login_usertext": "Log in as user",
+        "login_domainadmintext": "Log in as domain admin",
+        "login_admintext": "Log in as admin",
         "login_user": "User Login",
         "login_dadmin": "Domain-Administrator Login",
         "login_admin": "Administrator Login",
@@ -1105,6 +1116,7 @@
         "bcc_edited": "BCC map entry %s edited",
         "bcc_saved": "BCC map entry saved",
         "cors_headers_edited": "CORS settings have been saved",
+        "custom_login_modified": "Login customisation was saved successfully",
         "db_init_complete": "Database initialization completed",
         "delete_filter": "Deleted filters ID %s",
         "delete_filters": "Deleted filters: %s",

+ 35 - 1
data/web/templates/admin/tab-config-customize.twig

@@ -51,7 +51,41 @@
           </div></p>
         </form>
       </div>
-      <legend>{{ lang.admin.app_links }}</legend><hr />
+      <legend style="padding-top:20px" unselectable="on">{{ lang.admin.login_page }}</legend><hr />
+      <div>
+        <form class="form" data-id="custom_login" role="form" method="post">
+          <p class="text-muted">{{ lang.admin.quicklink_text }}</p>
+          <div class="ms-2 mb-1">
+            <input class="form-check-input" type="checkbox" value="1" name="hide_user_quicklink" id="hide_user_quicklink" {% if custom_login.hide_user_quicklink == 1 %}checked{% endif %}>
+            <label class="form-check-label" for="hide_user_quicklink">
+              {{ lang.admin.user_quicklink|raw }}
+            </label>
+          </div>
+          <div class="ms-2 mb-1">
+            <input class="form-check-input" type="checkbox" value="1" name="hide_domainadmin_quicklink" id="hide_domainadmin_quicklink" {% if custom_login.hide_domainadmin_quicklink == 1 %}checked{% endif %}>
+            <label class="form-check-label" for="hide_domainadmin_quicklink">
+              {{ lang.admin.domainadmin_quicklink|raw }}
+            </label>
+          </div>
+          <div class="ms-2 mb-4">
+            <input class="form-check-input" type="checkbox" value="1" name="hide_admin_quicklink" id="hide_admin_quicklink" {% if custom_login.hide_admin_quicklink == 1 %}checked{% endif %}>
+            <label class="form-check-label" for="hide_admin_quicklink">
+              {{ lang.admin.admin_quicklink|raw }}
+            </label>
+          </div>
+          <p class="text-muted">{{ lang.admin.force_sso_text|raw }}</p>
+          <div class="ms-2 mb-4">
+            <input class="form-check-input" type="checkbox" value="1" name="force_sso" id="force_sso" {% if custom_login.force_sso == 1 %}checked{% endif %}>
+            <label class="form-check-label" for="force_sso">
+              {{ lang.admin.force_sso|raw }}
+            </label>
+          </div>
+          <p><div class="btn-group">
+            <button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-item="admin" data-id="custom_login" data-reload="no" data-api-url='edit/custom_login' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
+          </div></p>
+        </form>
+      </div>
+      <legend style="padding-top:20px">{{ lang.admin.app_links }}</legend><hr />
       <p class="text-muted">{{ lang.admin.merged_vars_hint|raw }}</p>
       <form class="form-inline" data-id="app_links" role="form" method="post">
         <table class="table table-condensed" style="white-space: nowrap;" id="app_link_table">

+ 27 - 16
data/web/templates/admin_index.twig

@@ -5,13 +5,28 @@
 {% block content %}
 <div class="row mb-4" style="margin-top: 60px">
   <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
+
     <div class="card">
-      <div class="card-header d-flex align-items-center">
+      <div class="card-header d-flex align-items-center text-break">
         <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_admin }}
         <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
           <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
           <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
         </div>
+        <div class="ms-4 d-grid d-sm-block">
+          <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
+          </button>
+          <ul class="dropdown-menu ms-auto login">
+            {% for key, val in available_languages %}
+              <li>
+                <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
+                  <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
+                </a>
+              </li>
+            {% endfor %}
+          </ul>
+        </div>
       </div>
       <div class="card-body">
         <div class="text-center mailcow-logo mb-4">
@@ -37,23 +52,10 @@
             </div>
           </div>
           <div class="d-flex justify-content-between mt-4" style="position: relative">
-            <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>            <div class="d-grid d-sm-block">
-            <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-              <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
-            </button>
-            <ul class="dropdown-menu ms-auto login">
-              {% for key, val in available_languages %}
-                <li>
-                  <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
-                    <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
-                  </a>
-                </li>
-              {% endfor %}
-            </ul>
-            </div>
+            <button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
           </div>
         </form>
-        <div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div>
+        <div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
         <div class="d-flex flex-column align-items-center">
           <a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
         </div>
@@ -86,6 +88,15 @@
         {% endif %}
       </div>
     </div>
+
+    {% if custom_login.hide_user_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %}
+    <p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
+      {{ lang.login.login_linkstext }}<br>
+      {% if custom_login.hide_user_quicklink != 1 %}<a href="/">{{ lang.login.login_usertext }}</a>{% endif %}
+      {% if custom_login.hide_user_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %}
+      {% if custom_login.hide_domainadmin_quicklink != 1 %}<a href="/domainadmin">{{ lang.login.login_domainadmintext }}</a>{% endif %}
+    </p>
+    {% endif %}
   </div>
 </div>
 {% endblock %}

+ 27 - 16
data/web/templates/domainadmin_index.twig

@@ -5,13 +5,28 @@
 {% block content %}
 <div class="row mb-4" style="margin-top: 60px">
   <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
+
     <div class="card">
-      <div class="card-header d-flex align-items-center">
+      <div class="card-header d-flex align-items-center text-break">
         <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_dadmin }}
         <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
           <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
           <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
         </div>
+        <div class="ms-4 d-grid d-sm-block">
+          <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
+          </button>
+          <ul class="dropdown-menu ms-auto login">
+            {% for key, val in available_languages %}
+              <li>
+                <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
+                  <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
+                </a>
+              </li>
+            {% endfor %}
+          </ul>
+        </div>
       </div>
       <div class="card-body">
         <div class="text-center mailcow-logo mb-4">
@@ -37,23 +52,10 @@
             </div>
           </div>
           <div class="d-flex justify-content-between mt-4" style="position: relative">
-            <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>            <div class="d-grid d-sm-block">
-            <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-              <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
-            </button>
-            <ul class="dropdown-menu ms-auto login">
-              {% for key, val in available_languages %}
-                <li>
-                  <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
-                    <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
-                  </a>
-                </li>
-              {% endfor %}
-            </ul>
-            </div>
+            <button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
           </div>
         </form>
-        <div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div>
+        <div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
         <div class="d-flex flex-column align-items-center">
           <a class="btn btn-xs-lg btn-secondary w-100" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
         </div>
@@ -86,6 +88,15 @@
         {% endif %}
       </div>
     </div>
+
+    {% if custom_login.hide_user_quicklink != 1 or custom_login.hide_admin_quicklink != 1 %}
+    <p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
+      {{ lang.login.login_linkstext }}<br>
+      {% if custom_login.hide_user_quicklink != 1 %}<a href="/">{{ lang.login.login_usertext }}</a>{% endif %}
+      {% if custom_login.hide_user_quicklink != 1 and custom_login.hide_admin_quicklink != 1 %}|{% endif %}
+      {% if custom_login.hide_admin_quicklink != 1 %}<a href="/admin">{{ lang.login.login_admintext }}</a>{% endif %}
+    </p>
+    {% endif %}
   </div>
 </div>
 {% endblock %}

+ 38 - 22
data/web/templates/user_index.twig

@@ -5,13 +5,30 @@
 {% block content %}
 <div class="row mb-4" style="margin-top: 60px">
   <div class="col-12 col-md-7 col-lg-6 col-xl-5 ms-auto me-auto">
+
     <div class="card">
-      <div class="card-header d-flex align-items-center">
+      <div class="card-header d-flex align-items-center text-break">
         <i class="bi bi-person-fill me-2"></i> {{ lang.login.login_user }}
         <div class="ms-auto form-check form-switch my-auto d-flex align-items-center">
           <label class="form-check-label"><i class="bi bi-moon-fill"></i></label>
           <input class="form-check-input ms-2" type="checkbox" id="dark-mode-toggle">
         </div>
+        {% if not oauth2_request %}
+        <div class="ms-4 d-grid d-sm-block">
+          <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="text-secondary btn p-0 border-0 bg-transparent ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+            <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
+          </button>
+          <ul class="dropdown-menu ms-auto login">
+            {% for key, val in available_languages %}
+              <li>
+                <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
+                  <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
+                </a>
+              </li>
+            {% endfor %}
+          </ul>
+        </div>
+        {% endif %}
       </div>
       <div class="card-body">
         <div class="text-center mailcow-logo mb-4">
@@ -25,6 +42,7 @@
         {% if is_mobileconfig %}
         <div class="my-4 alert alert-info ">{{ lang.login.mobileconfig_info }}</div>
         {% endif %}
+        {% if custom_login.force_sso != 1 %}
         <form method="post" autofill="off">
           <div class="d-flex mt-3">
             <label class="visually-hidden" for="login_user">{{ lang.login.username }}</label>
@@ -40,35 +58,22 @@
               <input name="pass_user" type="password" id="pass_user" class="form-control" placeholder="{{ lang.login.password }}" required="" autocomplete="current-password">
             </div>
           </div>
+          <div class="mt-2 text-muted" style="font-size: 0.9rem;">
+            <a href="/reset-password">{{ lang.login.forgot_password }}</a>
+          </div>
           <div class="d-flex justify-content-between mt-4" style="position: relative">
-            <button type="submit" class="btn btn-xs-lg btn-success" value="Login">{{ lang.login.login }}</button>
-            {% if not oauth2_request %}
-            <div class="d-grid d-sm-block">
-            <button type="button" {% if available_languages|length == 1 %}disabled="true"{% endif %} class="btn btn-secondary ms-auto dropdown-toggle" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-              <span class="flag-icon flag-icon-{{ mailcow_locale[-2:] }}"></span>
-            </button>
-            <ul class="dropdown-menu ms-auto login">
-              {% for key, val in available_languages %}
-                <li>
-                  <a class="dropdown-item {% if mailcow_locale == key %}active{% endif %}" href="?{{ query_string({'lang': key}) }}">
-                    <span class="flag-icon flag-icon-{{ key[-2:] }}"></span>{{ val }}
-                  </a>
-                </li>
-              {% endfor %}
-            </ul>
-            </div>
-            {% endif %}
+            <button type="submit" class="btn btn-xs-lg btn-success w-100 mt-2 mx-auto" style="max-width: 400px;" value="Login">{{ lang.login.login }}</button>
           </div>
         </form>
-        <div class="mt-3">
-          <a href="/reset-password">{{ lang.login.forgot_password }}</a>
-        </div>
-        <div class="hr-title mt-5"><strong>{{ lang.login.other_logins }}</strong></div>
+        <div class="hr-title"><strong>{{ lang.login.other_logins }}</strong></div>
+        {% endif %}
         <div class="d-flex flex-column align-items-center">
           {% if has_iam_sso %}
           <a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="/?iam_sso=1"><i class="bi bi-cloud-arrow-up-fill"></i> {{ lang.admin.iam_sso }}</a>
           {% endif %}
+          {% if custom_login.force_sso != 1 %}
           <a class="btn btn-xs-lg btn-secondary w-100 mt-2" style="max-width: 400px;" href="#" id="fido2-login"><i class="bi bi-shield-fill-check"></i> {{ lang.login.fido2_webauthn }}</a>
+          {% endif %}
         </div>
         {% if login_delay %}
         <p><div class="my-4 alert alert-info">{{ lang.login.delayed|format(login_delay) }}</b></div></p>
@@ -96,9 +101,20 @@
             {% endfor %}
           {% endfor %}
         </div>
+        <div>
+        </div>
         {% endif %}
       </div>
     </div>
+
+    {% if custom_login.hide_admin_quicklink != 1 or custom_login.hide_domainadmin_quicklink != 1 %}
+    <p class="text-center mt-3 text-muted" style="font-size: 0.9rem;">
+      {{ lang.login.login_linkstext }}<br>
+      {% if custom_login.hide_admin_quicklink != 1 %}<a href="/admin">{{ lang.login.login_admintext }}</a>{% endif %}
+      {% if custom_login.hide_admin_quicklink != 1 and custom_login.hide_domainadmin_quicklink != 1 %}|{% endif %}
+      {% if custom_login.hide_domainadmin_quicklink != 1 %}<a href="/domainadmin">{{ lang.login.login_domainadmintext }}</a>{% endif %}
+    </p>
+    {% endif %}
   </div>
 </div>
 {% if not oauth2_request and ui_texts.help_text %}