first commit
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user