143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
package webui
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
"mailgoserver/internal/db"
|
|
)
|
|
|
|
// mailboxPendingMFACookieName mirrors pendingMFACookieName for the mailbox portal —
|
|
// kept fully separate so an unfinished mailbox login can never be confused with (or
|
|
// promoted into) an admin session, and vice versa.
|
|
const mailboxPendingMFACookieName = "mailgoserver_mailbox_pending_mfa"
|
|
|
|
func setMailboxPendingMFACookie(w http.ResponseWriter, mailboxID string) {
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: mailboxPendingMFACookieName, Value: mailboxID, Path: "/", HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode, MaxAge: 10 * 60,
|
|
})
|
|
}
|
|
func clearMailboxPendingMFACookie(w http.ResponseWriter) {
|
|
http.SetCookie(w, &http.Cookie{Name: mailboxPendingMFACookieName, Value: "", Path: "/", MaxAge: -1})
|
|
}
|
|
func pendingMailboxMFAID(r *http.Request) int64 {
|
|
c, err := r.Cookie(mailboxPendingMFACookieName)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return int64(atoi(c.Value))
|
|
}
|
|
|
|
func (a *App) webmailLoginForm(w http.ResponseWriter, r *http.Request) {
|
|
if sess, mbox, _ := a.currentMailboxSession(r); sess != nil && mbox != nil {
|
|
http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound)
|
|
return
|
|
}
|
|
a.render(w, r, "webmail_login.html", M{})
|
|
}
|
|
|
|
// webmailLoginSubmit checks email+password against the mailbox's own portal
|
|
// password (never an app password — that's for IMAP/SMTP clients only).
|
|
func (a *App) webmailLoginSubmit(w http.ResponseWriter, r *http.Request) {
|
|
email := strings.TrimSpace(r.FormValue("email"))
|
|
password := r.FormValue("password")
|
|
|
|
fail := func(msg string) {
|
|
a.render(w, r, "webmail_login.html", M{"error": msg, "email": email})
|
|
}
|
|
|
|
mbox, err := a.DB.GetMailboxByEmail(email)
|
|
if err != nil {
|
|
a.Logger.Error("webmail login lookup: %v", err)
|
|
fail("Something went wrong. Try again.")
|
|
return
|
|
}
|
|
if mbox == nil || !db.CheckPassword(password, mbox.PasswordHash) {
|
|
_ = a.DB.LogAuthAttempt("webmail_login", email, requestIP(r), false, "Incorrect email or password")
|
|
fail("Incorrect email or password.")
|
|
return
|
|
}
|
|
|
|
needsMFA := mbox.TOTPEnabled
|
|
if !needsMFA {
|
|
if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 {
|
|
needsMFA = true
|
|
}
|
|
}
|
|
|
|
if !needsMFA {
|
|
token, err := a.DB.CreateMailboxSession(mbox.ID, true, sessionTTL)
|
|
if err != nil {
|
|
fail("Something went wrong. Try again.")
|
|
return
|
|
}
|
|
_ = a.DB.LogAuthAttempt("webmail_login", email, requestIP(r), true, "Login successful")
|
|
setMailboxSessionCookie(w, token, r.TLS != nil)
|
|
http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
setMailboxPendingMFACookie(w, strconv.FormatInt(mbox.ID, 10))
|
|
http.Redirect(w, r, MailboxPrefix+"/login/mfa", http.StatusFound)
|
|
}
|
|
|
|
func (a *App) webmailMFAForm(w http.ResponseWriter, r *http.Request) {
|
|
mailboxID := pendingMailboxMFAID(r)
|
|
if mailboxID == 0 {
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
return
|
|
}
|
|
mbox, _ := a.DB.GetMailboxByID(mailboxID)
|
|
if mbox == nil {
|
|
clearMailboxPendingMFACookie(w)
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
return
|
|
}
|
|
hasPasskeys, _ := a.DB.CountMailboxWebAuthnCredentials(mailboxID)
|
|
a.render(w, r, "webmail_login_mfa.html", M{"totp_enabled": mbox.TOTPEnabled, "has_passkeys": hasPasskeys > 0})
|
|
}
|
|
|
|
func (a *App) webmailMFASubmit(w http.ResponseWriter, r *http.Request) {
|
|
mailboxID := pendingMailboxMFAID(r)
|
|
if mailboxID == 0 {
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
return
|
|
}
|
|
mbox, err := a.DB.GetMailboxByID(mailboxID)
|
|
if err != nil || mbox == nil {
|
|
clearMailboxPendingMFACookie(w)
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
return
|
|
}
|
|
|
|
code := strings.TrimSpace(r.FormValue("code"))
|
|
if !mbox.TOTPEnabled || !totp.Validate(code, mbox.TOTPSecret) {
|
|
_ = a.DB.LogAuthAttempt("webmail_login", mbox.Email, requestIP(r), false, "Invalid MFA code")
|
|
hasPasskeys, _ := a.DB.CountMailboxWebAuthnCredentials(mailboxID)
|
|
a.render(w, r, "webmail_login_mfa.html", M{"totp_enabled": mbox.TOTPEnabled, "has_passkeys": hasPasskeys > 0, "error": "Invalid code."})
|
|
return
|
|
}
|
|
|
|
token, err := a.DB.CreateMailboxSession(mbox.ID, true, sessionTTL)
|
|
if err != nil {
|
|
a.Logger.Error("create mailbox session: %v", err)
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
return
|
|
}
|
|
_ = a.DB.LogAuthAttempt("webmail_login", mbox.Email, requestIP(r), true, "Login successful (authenticator app)")
|
|
clearMailboxPendingMFACookie(w)
|
|
setMailboxSessionCookie(w, token, r.TLS != nil)
|
|
http.Redirect(w, r, MailboxPrefix+"/", http.StatusFound)
|
|
}
|
|
|
|
func (a *App) webmailLogout(w http.ResponseWriter, r *http.Request) {
|
|
if c, err := r.Cookie(mailboxSessionCookieName); err == nil {
|
|
_ = a.DB.DeleteMailboxSession(c.Value)
|
|
}
|
|
clearMailboxSessionCookie(w)
|
|
http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound)
|
|
}
|