package webui import ( "bytes" "encoding/base64" "image/png" "net/http" "strings" "github.com/pquerna/otp/totp" "mailgoserver/internal/db" ) // accountPage shows the admin their own profile: password change, TOTP MFA // enable/disable, and registered passkeys (passkey registration itself is wired up // in webauthn.go). func (a *App) accountPage(w http.ResponseWriter, r *http.Request) { user := userFromContext(r) creds, _ := a.DB.ListWebAuthnCredentials(user.ID) a.render(w, r, "account.html", M{"active": "account", "user": user, "passkeys": creds}) } // changePassword mirrors a normal (not forced) password change from account settings. func (a *App) changePassword(w http.ResponseWriter, r *http.Request) { user := userFromContext(r) current := r.FormValue("current_password") newPassword := r.FormValue("new_password") confirm := r.FormValue("new_password_confirm") if !db.CheckPassword(current, user.PasswordHash) { setFlash(w, "error", "Current password is incorrect") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } if !isStrongPassword(newPassword) { setFlash(w, "error", "New password must be at least 10 characters and include a letter, a number, and a symbol") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } if newPassword != confirm { setFlash(w, "error", "New passwords don't match") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } hash, err := db.HashPassword(newPassword) if err != nil { setFlash(w, "error", "Something went wrong") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } if err := a.DB.UpdateAdminPassword(user.ID, hash); err != nil { setFlash(w, "error", "Something went wrong") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } setFlash(w, "success", "Password updated") http.Redirect(w, r, Prefix+"/account", http.StatusFound) } // totpSetupBegin generates a fresh (not-yet-enabled) TOTP secret and shows it as a // scannable QR code (rendered inline as a data: URI — simplest way to hand the // browser an image without a second round-trip route) plus the manual entry key. func (a *App) totpSetupBegin(w http.ResponseWriter, r *http.Request) { user := userFromContext(r) key, err := totp.Generate(totp.GenerateOpts{ Issuer: "mailgoserver", AccountName: user.Username, }) if err != nil { setFlash(w, "error", "Could not generate a TOTP secret") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } if err := a.DB.SetAdminTOTPSecret(user.ID, key.Secret(), false); err != nil { setFlash(w, "error", "Could not save the TOTP secret") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } img, err := key.Image(256, 256) qrDataURI := "" if err == nil { var buf bytes.Buffer if png.Encode(&buf, img) == nil { qrDataURI = "data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()) } } a.render(w, r, "totp_setup.html", M{"secret": key.Secret(), "qr_data_uri": qrDataURI}) } // totpSetupConfirm verifies a code against the pending secret and, if correct, flips // TOTP on for the account. func (a *App) totpSetupConfirm(w http.ResponseWriter, r *http.Request) { user := userFromContext(r) code := strings.TrimSpace(r.FormValue("code")) if user.TOTPSecret == "" || !totp.Validate(code, user.TOTPSecret) { setFlash(w, "error", "That code didn't match — try scanning the QR code again") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } if err := a.DB.SetAdminTOTPSecret(user.ID, user.TOTPSecret, true); err != nil { setFlash(w, "error", "Something went wrong enabling MFA") http.Redirect(w, r, Prefix+"/account", http.StatusFound) return } setFlash(w, "success", "Authenticator app MFA enabled") http.Redirect(w, r, Prefix+"/account", http.StatusFound) } func (a *App) totpDisable(w http.ResponseWriter, r *http.Request) { user := userFromContext(r) if err := a.DB.DisableAdminTOTP(user.ID); err != nil { setFlash(w, "error", "Something went wrong") } else { setFlash(w, "success", "Authenticator app MFA disabled") } http.Redirect(w, r, Prefix+"/account", http.StatusFound) }