package webui import ( "context" "net/http" "time" "mailgoserver/internal/db" ) // MailboxPrefix is the self-service webmail portal's URL prefix — a mailbox owner's // login/account area, entirely separate from the admin dashboard at Prefix. const MailboxPrefix = "/webmail" const mailboxSessionCookieName = "mailgoserver_mailbox_session" // mailboxCtxKey is its own type (not webui's ctxKey) so a mailbox session can never // collide with or be confused for an admin session in request context — the two // actor types are deliberately kept fully separate, per the parallel-schema design. type mailboxCtxKey int const ctxMailboxKey mailboxCtxKey = iota func setMailboxSessionCookie(w http.ResponseWriter, token string, secure bool) { http.SetCookie(w, &http.Cookie{ Name: mailboxSessionCookieName, Value: token, Path: "/", HttpOnly: true, Secure: secure, SameSite: http.SameSiteLaxMode, MaxAge: int(sessionTTL.Seconds()), }) } func clearMailboxSessionCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{Name: mailboxSessionCookieName, Value: "", Path: "/", MaxAge: -1}) } // currentMailboxSession loads the session + mailbox for the request's cookie, if any // and valid. A nil session/mailbox (no error) means "not logged in". func (a *App) currentMailboxSession(r *http.Request) (*db.MailboxSession, *db.Mailbox, error) { c, err := r.Cookie(mailboxSessionCookieName) if err != nil || c.Value == "" { return nil, nil, nil } sess, err := a.DB.GetMailboxSession(c.Value) if err != nil || sess == nil { return nil, nil, err } if time.Now().After(sess.ExpiresAt) { _ = a.DB.DeleteMailboxSession(sess.Token) return nil, nil, nil } mbox, err := a.DB.GetMailboxByID(sess.MailboxID) if err != nil || mbox == nil { return nil, nil, err } return sess, mbox, nil } func mailboxFromContext(r *http.Request) *db.Mailbox { m, _ := r.Context().Value(ctxMailboxKey).(*db.Mailbox) return m } // requireMailboxAuth gates every webmail route behind a valid, fully-authenticated // mailbox session: logged in, and second factor satisfied if one is enabled. func (a *App) requireMailboxAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { sess, mbox, err := a.currentMailboxSession(r) if err != nil { a.Logger.Error("mailbox session lookup: %v", err) } if sess == nil || mbox == nil { http.Redirect(w, r, MailboxPrefix+"/login", http.StatusFound) return } needsMFA := mbox.TOTPEnabled if !needsMFA { if n, _ := a.DB.CountMailboxWebAuthnCredentials(mbox.ID); n > 0 { needsMFA = true } } if needsMFA && !sess.MFAVerified { http.Redirect(w, r, MailboxPrefix+"/login/mfa", http.StatusFound) return } ctx := context.WithValue(r.Context(), ctxMailboxKey, mbox) next.ServeHTTP(w, r.WithContext(ctx)) }) }