From ee5009a47f45c1bcfe8b2113245d0ff42323baf7 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 1 Oct 2026 00:28:17 +0200 Subject: [PATCH] Fix login, password reset and error pages in framed theme --- gray-theme/unauthenticated/gray-theme.css | 262 ++++++++++++++++++++++ pam_login.cgi | 12 +- session_login.cgi | 17 +- 3 files changed, 283 insertions(+), 8 deletions(-) diff --git a/gray-theme/unauthenticated/gray-theme.css b/gray-theme/unauthenticated/gray-theme.css index 08aa6955c..9cf39151f 100644 --- a/gray-theme/unauthenticated/gray-theme.css +++ b/gray-theme/unauthenticated/gray-theme.css @@ -2038,9 +2038,271 @@ table.shrinkwrapper.loginform { max-width: 440px; margin: 24px auto var(--card-gap); } +/* Notices above the login form, the certificate warning and the login + * messages the script tags as failed, logout or timed out, take the + * card's width and sit a card gap above it. The messages are drawn by + * the error page rules below. The empty paragraph after a message is + * dropped. */ +body:has(> center > form.ui_form[action*="_login.cgi"]) > center > :is(.ui_alert_box, h3.ui_login_message) { + max-width: 640px; + margin: var(--card-gap) auto calc(var(--card-gap) * 2) auto; + text-align: start; +} +body:has(> center > form.ui_form[action*="_login.cgi"]) > center > h3.ui_login_message + p:empty { + display: none; +} +body:has(> center > form.ui_form[action*="_login.cgi"]) > center > .ui_alert_box + h3.ui_login_message { + margin-top: var(--card-gap); +} +body:has(> center > form.ui_form[action*="_login.cgi"]) > center > :is(.ui_alert_box + form.ui_form, h3.ui_login_message + p + form.ui_form) > table.shrinkwrapper.loginform { + margin-top: var(--card-gap); +} table.header:has(#headln2c > font:empty):not(:has(a, img, #headln1)) { display: none; } +/* The login button is the main action of the page, and the forgotten + * password form under the card is a plain link with room above it, + * not a filled button; its selectors outrank the primary button rule */ +form.ui_form[action*="session_login.cgi"] > input.ui_submit { + background: var(--accent-color); + border-color: var(--accent-color-hover); + color: var(--text-color-inverse); + font-weight: 600; +} +form.ui_form[action*="session_login.cgi"] > input.ui_submit:hover { + background: var(--accent-color-hover); +} +form.ui_form[action*="forgot_form.cgi"] { margin-top: 14px; } +form.ui_form[action*="forgot_form.cgi"] table.ui_form_end_buttons td:first-child > input.ui_submit { + min-height: 0; + padding: 4px 8px; + border-color: transparent; + background: transparent; + color: var(--link-color); + font-weight: 500; +} +form.ui_form[action*="forgot_form.cgi"] table.ui_form_end_buttons td:first-child > input.ui_submit:hover { + border-color: transparent; + background: transparent; + color: var(--link-color-hover); + text-decoration: underline; +} +/* The password reset flow: the form asking for the username, the page + * confirming the email, and the new password form from the emailed + * link. Each prints its title in the page header and its content inside + * a center block. The header becomes the head row of a card like the + * login form's, with the card's edge and top corners, and the center + * block continues the card under it. The confirmation page is told + * apart by its content, text with two tt elements and no form. The + * empty paragraph the core prints after the header is dropped, and a + * warning above the form takes the card's width. */ +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > table.header { + position: static; + box-sizing: border-box; + width: 100%; + max-width: 440px; + margin: 24px auto 0; + padding: 9px 16px; + background: var(--main-content); + border: 1px solid var(--border-color); + border-bottom-color: var(--border-color-light); + border-radius: var(--radius) var(--radius) 0 0; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > table.header #headln2c { + padding: 0; + font-size: 14px; + font-weight: 600; + letter-spacing: 0; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > table.header + p:empty { + display: none; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > .ui_alert_box { + max-width: 440px; + margin: 0 auto; + border-radius: 0; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center { + box-sizing: border-box; + max-width: 440px; + margin: 0 auto var(--card-gap); + padding: 12px 16px 6px; + background: var(--main-content); + border: 1px solid var(--border-color); + border-top: 0; + border-radius: 0 0 var(--radius) var(--radius); + box-shadow: var(--shadow); + text-align: start; +} +/* The username form: the field on a line of its own under its label. + * The new password form: its own card table melts into the card. Both: + * the button centered under the fields with room above it, like the + * buttons under the login card. The confirmation text keeps the card's + * bottom padding. */ +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > b { + display: block; + margin: 12px 0 4px; + font-weight: 400; + color: var(--text-color-desc); +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > input.ui_textbox { + width: 100%; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.shrinkwrapper { + margin: 0; + border: 0; + background: transparent; + box-shadow: none; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.shrinkwrapper tr.ui_table_body > td { + padding: 0; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.ui_form_end_buttons { + margin: 10px 0 8px; +} +:is(body:has(> center > form.ui_form[action*="forgot_send.cgi"]), + body:has(> center > form.ui_form[action*="forgot.cgi"]), + body:has(> table.header + p + center > tt:first-child)) > center > form.ui_form > table.ui_form_end_buttons > tbody > tr { + justify-content: center; +} +body:has(> table.header + p + center > tt:first-child) > center { + padding-bottom: 14px; + line-height: 1.5; +} + +/* ---- Error page ---- */ +/* The core prints an error as a heading between rules, with an optional + * call stack table, and its own inline style after the theme's. The + * heading becomes a danger notice with the notice icon, the rules are + * dropped, and the stack becomes a card table under a small label. The + * selectors repeat the classes of the inline style, so they outrank it. */ + +hr:has(+ h3[data-fatal-error-text]), h3[data-fatal-error-text] + hr, +hr:has(+ table.err-stack), table.err-stack + hr { display: none; } +/* The notice sits close under the title; the inline style block the + * core prints right after the header marks the error page */ +table.header:has(+ style[data-err]) { margin-bottom: 2px; } +h3[data-fatal-error-text], h3.err-head[data-fatal-error-text], +h3.ui_login_message { + display: flex; + box-sizing: border-box; + align-items: flex-start; + gap: 12px; + margin: 0 0 var(--card-gap); + padding: 10px 14px; + border: 1px solid var(--border-color-danger); + border-radius: var(--radius); + background: var(--bg-color-danger); + color: var(--text-color-danger-stressed); + font: 500 13px/1.45 var(--font-family); + text-transform: none; + transform: none; + white-space: normal; + overflow-wrap: break-word; +} +/* The login messages: a logout is a success notice and a timed out + * session a warning; a failed login keeps the danger look */ +h3.ui_login_logout { + background: var(--bg-color-success); + border-color: var(--border-color-success); + color: var(--text-color-success-stressed); +} +h3.ui_login_timed_out { + background: var(--bg-color-warning); + border-color: var(--border-color-warning); + color: var(--text-color-warning-stressed); +} +/* The icon is one of the theme's notice icons, drawn as a mask in the + * notice color: x-circle, check-circle or the warning triangle */ +h3[data-fatal-error-text]::before, h3.ui_login_message::before { + content: ""; + flex: 0 0 auto; + width: 18px; + height: 18px; + margin-top: 1px; + background: var(--text-color-danger); + -webkit-mask: url("data:image/svg+xml;utf8,") center / contain no-repeat; + mask: url("data:image/svg+xml;utf8,") center / contain no-repeat; +} +h3.ui_login_logout::before { + background: var(--text-color-success); + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); +} +h3.ui_login_timed_out::before { + background: var(--text-color-warning); + -webkit-mask-image: url("data:image/svg+xml;utf8,"); + mask-image: url("data:image/svg+xml;utf8,"); +} +h3[data-fatal-error-text] pre { + margin: 0; + font-size: 12px; + white-space: pre-wrap; +} +table.err-stack, table.err-stack.captured { + width: 100%; + margin: 0 0 var(--card-gap); + border: 1px solid var(--border-color); + border-radius: var(--radius); + border-collapse: separate; + border-spacing: 0; + background: var(--main-content); + box-shadow: var(--shadow); +} +table.err-stack > caption.err-head, table.err-stack > caption.err-head.captured { + caption-side: top; + padding: 0 0 8px var(--title-inset); + color: var(--text-color-muted); + font: 600 12px/1.45 var(--font-family); + letter-spacing: 0.06em; + text-align: start; + text-transform: uppercase; + transform: none; +} +table.err-stack > tbody > tr > td, table.err-stack.captured > tbody > tr > td.captured { + padding: 4px 16px; + border-bottom: 1px solid var(--border-color-lighter); + color: var(--text-color); + font: 12.5px/1.45 var(--font-family-mono); + transform: none; +} +table.err-stack > tbody > tr:first-child > td, +table.err-stack.captured > tbody > tr:first-child > td.captured { + height: auto; + padding: 8px 16px; + border-bottom: 1px solid var(--border-color); + color: var(--text-color-muted); + font: 600 12px/1.45 var(--font-family); + text-transform: none; + vertical-align: middle; +} +table.err-stack > tbody > tr:first-child > td > b, +table.err-stack.captured > tbody > tr:first-child > td.captured > b { + border: 0; + color: inherit; + font-weight: inherit; +} +table.err-stack > tbody > tr:last-child > td { border-bottom: 0; } /* ---- Module specific rules ---- */ diff --git a/pam_login.cgi b/pam_login.cgi index 4b3b40814..c6e0cdf1b 100755 --- a/pam_login.cgi +++ b/pam_login.cgi @@ -59,14 +59,20 @@ if (&miniserv_using_default_cert()) { print "

",&text('defcert_error', ucfirst(&get_product_name()), ($ENV{'MINISERV_KEYFILE'} || $miniserv{'keyfile'})),"

\n"; } +# Message above the form, with classes naming its kind so a theme can +# draw it as a notice if (defined($in{'failed'})) { - print "

$text{'session_failed'}

\n"; + print "

", + "$text{'session_failed'}

\n"; } elsif ($in{'logout'}) { - print "

$text{'session_logout'}

\n"; + print "

", + "$text{'session_logout'}

\n"; } elsif ($in{'timed_out'}) { - print "

",&text('session_timed_out', int($in{'timed_out'}/60)),"

\n"; + print "

", + &text('session_timed_out', int($in{'timed_out'}/60)), + "

\n"; } print "$text{'pam_prefix'}\n"; diff --git a/session_login.cgi b/session_login.cgi index 31e7a48df..7d6e47d34 100755 --- a/session_login.cgi +++ b/session_login.cgi @@ -74,20 +74,27 @@ if (&miniserv_using_default_cert()) { ucfirst(&get_product_name()), ($ENV{'MINISERV_KEYFILE'} || $miniserv{'keyfile'})), 'warn'); } +# Message above the form, with classes naming its kind so a theme can +# draw it as a notice if (defined($in{'failed'})) { if ($in{'twofactor_msg'}) { - print "

",&text('session_twofailed', - &html_escape($in{'twofactor_msg'})),"

\n"; + print "

", + &text('session_twofailed', + &html_escape($in{'twofactor_msg'})),"

\n"; } else { - print "

$text{'session_failed'}

\n"; + print "

", + "$text{'session_failed'}

\n"; } } elsif ($in{'logout'}) { - print "

$text{'session_logout'}

\n"; + print "

", + "$text{'session_logout'}

\n"; } elsif ($in{'timed_out'}) { - print "

",&text('session_timed_out', int($in{'timed_out'}/60)),"

\n"; + print "

", + &text('session_timed_out', int($in{'timed_out'}/60)), + "

\n"; } print "$text{'session_prefix'}\n";