2026-08-13 10:40:27 +01:00
{{define "webmail_mfa_setup_required.html"}}
<!DOCTYPE html>
< html lang = "en" data-bs-theme = "dark" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Set up two-factor authentication - Webmail</ title >
2026-08-14 13:04:55 +01:00
< link href = "/webmail/static/vendor/bootstrap/css/bootstrap.min.css" rel = "stylesheet" >
< link href = "/webmail/static/vendor/bootstrap-icons/font/bootstrap-icons.css" rel = "stylesheet" >
2026-08-13 10:40:27 +01:00
< style >
body { background-color : #1a1a1a ; color : #e0e0e0 ; min-height : 100 vh ; display : flex ; align-items : center ; }
. setup-card { max-width : 480 px ; margin : 0 auto ; width : 100 % ; }
. card { background-color : #2d2d2d ; border : 1 px solid #404040 ; }
</ style >
</ head >
< body >
2026-08-14 13:04:55 +01:00
{{template "csrf_script" .}}
2026-08-13 10:40:27 +01:00
< div class = "toast-container position-fixed top-0 end-0 p-3" style = "z-index: 1090;" >
{{range .flashes}}
< div class = "toast align-items-center text-bg-{{if eq .Category " error "}} danger {{ else }}{{. Category }}{{ end }} border-0 " role = "alert" aria-live = "assertive" aria-atomic = "true" data-bs-autohide = "false" >
< div class = "d-flex" >
< div class = "toast-body" >
< i class = "bi bi-{{if eq .Category " error "}} exclamation-triangle {{ else if eq . Category " success "}} check-circle {{ else }} info-circle {{ end }} me-2 " ></ i >
{{.Message}}
</ div >
< button type = "button" class = "btn-close btn-close-white me-2 m-auto" data-bs-dismiss = "toast" aria-label = "Close" ></ button >
</ div >
</ div >
{{end}}
</ div >
< div class = "container setup-card" >
< div class = "text-center mb-4" >
< i class = "bi bi-shield-lock-fill" style = "font-size: 2.5rem;" ></ i >
< h4 class = "mt-2" > Two-factor authentication required</ h4 >
< p class = "text-muted" > Your administrator requires MFA for this mailbox (< strong > {{.email}}</ strong > ). Set up one of the options below to continue — nothing else is accessible until then. Any app passwords you already have keep working for email as normal.</ p >
</ div >
< div class = "card mb-3" >
< div class = "card-header" >< h5 class = "mb-0" >< i class = "bi bi-fingerprint me-2" ></ i > Passkey < span class = "badge bg-primary ms-1" > Recommended</ span ></ h5 ></ div >
< div class = "card-body" >
< p class = "text-muted" > Use your device's built-in security (fingerprint, face, or a hardware security key).</ p >
< button type = "button" class = "btn btn-primary" id = "passkey-add-btn" >< i class = "bi bi-fingerprint me-1" ></ i > Set up a Passkey</ button >
< div id = "passkey-error" class = "alert alert-danger d-none mt-2" ></ div >
</ div >
</ div >
< div class = "card" >
< div class = "card-header" >< h5 class = "mb-0" >< i class = "bi bi-qr-code me-2" ></ i > Authenticator App</ h5 ></ div >
< div class = "card-body" >
< p class = "text-muted" > Use Google Authenticator, 1Password, or any TOTP app.</ p >
< form method = "post" action = "/webmail/account/totp/setup" >
< button type = "submit" class = "btn btn-outline-primary" >< i class = "bi bi-qr-code me-1" ></ i > Set up Authenticator App</ button >
</ form >
</ div >
</ div >
</ div >
2026-08-14 13:04:55 +01:00
< script src = "/webmail/static/vendor/bootstrap/js/bootstrap.bundle.min.js" ></ script >
2026-08-13 10:40:27 +01:00
< script >
document . addEventListener ( 'DOMContentLoaded' , function () {
document . querySelectorAll ( '.toast' ). forEach ( function ( el ) { new bootstrap . Toast ( el , { delay : 6000 }). show (); });
});
function b64urlToBuf ( s ) {
s = s . replace ( /-/g , '+' ). replace ( /_/g , '/' );
while ( s . length % 4 ) s += '=' ;
const bin = atob ( s );
const buf = new Uint8Array ( bin . length );
for ( let i = 0 ; i < bin . length ; i ++ ) buf [ i ] = bin . charCodeAt ( i );
return buf . buffer ;
}
function bufToB64url ( buf ) {
const bytes = new Uint8Array ( buf );
let bin = '' ;
bytes . forEach ( b => bin += String . fromCharCode ( b ));
return btoa ( bin ). replace ( /\+/g , '-' ). replace ( /\//g , '_' ). replace ( /=+$/ , '' );
}
document . getElementById ( 'passkey-add-btn' ). addEventListener ( 'click' , async function () {
const errEl = document . getElementById ( 'passkey-error' );
errEl . classList . add ( 'd-none' );
try {
const beginResp = await fetch ( '/webmail/account/passkey/begin' , { method : 'POST' });
if ( ! beginResp . ok ) throw new Error (( await beginResp . json ()). error || 'Could not start passkey registration' );
const options = await beginResp . json ();
const publicKey = options . publicKey ;
publicKey . challenge = b64urlToBuf ( publicKey . challenge );
publicKey . user . id = b64urlToBuf ( publicKey . user . id );
if ( publicKey . excludeCredentials ) {
publicKey . excludeCredentials = publicKey . excludeCredentials . map ( c => ({ ... c , id : b64urlToBuf ( c . id ) }));
}
const cred = await navigator . credentials . create ({ publicKey });
const body = {
id : cred . id , rawId : bufToB64url ( cred . rawId ), type : cred . type ,
response : {
attestationObject : bufToB64url ( cred . response . attestationObject ),
clientDataJSON : bufToB64url ( cred . response . clientDataJSON ),
},
};
const finishResp = await fetch ( '/webmail/account/passkey/finish' , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' }, body : JSON . stringify ( body ),
});
if ( ! finishResp . ok ) throw new Error (( await finishResp . json ()). error || 'Could not save passkey' );
window . location . href = '/webmail/' ;
} catch ( e ) {
errEl . textContent = e . message || 'Passkey registration failed' ;
errEl . classList . remove ( 'd-none' );
}
});
</ script >
</ body >
</ html >
{{end}}