Dialog buttons are Cancel then the primary action at the bottom right, and each page has one primary action. List pages no longer repeat their title. Dates have the month written out. Fields in a row line up, hints sit under fields instead of inside labels, and long certificate errors are clamped. Organizations show whether local access is on and name the action; setting a user's password uses a dialog instead of a browser prompt. Server errors for sign-in and ranges are reworded with a next step, and the stray scrollbar inside tables is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
132 lines
3.4 KiB
Go
132 lines
3.4 KiB
Go
package hammerhead
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/smtp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (s *Server) getSettings(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
st := s.st.Settings
|
|
st.SMTP.Pass = "" // write-only
|
|
writeJSON(w, 200, map[string]any{"settings": st, "smtpPassSet": s.st.Settings.SMTP.Pass != ""})
|
|
}
|
|
|
|
func (s *Server) putSettings(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
var in Settings
|
|
if !decode(w, r, &in) {
|
|
return
|
|
}
|
|
in.DashboardURL = strings.TrimRight(strings.TrimSpace(in.DashboardURL), "/")
|
|
if in.DashboardURL != "" && !strings.HasPrefix(in.DashboardURL, "http://") && !strings.HasPrefix(in.DashboardURL, "https://") {
|
|
fail(w, 400, errors.New("dashboard URL must start with http:// or https://"))
|
|
return
|
|
}
|
|
if m := in.SMTP.Mode; m != "" && m != "starttls" && m != "tls" && m != "none" {
|
|
fail(w, 400, errors.New("smtp mode must be starttls, tls or none"))
|
|
return
|
|
}
|
|
s.mu.Lock()
|
|
if in.SMTP.Pass == "" {
|
|
in.SMTP.Pass = s.st.Settings.SMTP.Pass // blank keeps the saved password
|
|
}
|
|
if s.opts.DashboardURL != "" {
|
|
in.DashboardURL = s.st.Settings.DashboardURL // pinned by flag/env
|
|
}
|
|
in.SelfEdgeID = s.st.Settings.SelfEdgeID // not editable here; losing it would make the next start create a second self edge
|
|
s.st.Settings = in
|
|
s.bump() // edges receive the dashboard URL and ACME settings
|
|
s.mu.Unlock()
|
|
s.audit("", p.User.Email, "settings.update", "")
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) testMail(w http.ResponseWriter, r *http.Request, p *principal) {
|
|
if err := s.Mailer(p.User.Email, "Hammerhead test email", "If you can read this, outgoing email works."); err != nil {
|
|
fail(w, 400, err)
|
|
return
|
|
}
|
|
ok(w)
|
|
}
|
|
|
|
func (s *Server) mailConfigured() bool {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
return s.st.Settings.SMTP.Host != ""
|
|
}
|
|
|
|
// smtpSend is the default Mailer.
|
|
func (s *Server) smtpSend(to, subject, body string) error {
|
|
s.mu.Lock()
|
|
c := s.st.Settings.SMTP
|
|
s.mu.Unlock()
|
|
if c.Host == "" {
|
|
return errors.New("Email is not set up yet. Set it up under Server, Settings.")
|
|
}
|
|
if c.Port == 0 {
|
|
c.Port = 587
|
|
if c.Mode == "tls" {
|
|
c.Port = 465
|
|
}
|
|
}
|
|
if c.From == "" {
|
|
c.From = c.User
|
|
}
|
|
addr := net.JoinHostPort(c.Host, strconv.Itoa(c.Port))
|
|
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s\r\n",
|
|
c.From, to, strings.NewReplacer("\r", "", "\n", " ").Replace(subject), body)
|
|
|
|
d := net.Dialer{Timeout: 10 * time.Second}
|
|
conn, err := d.Dial("tcp", addr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if c.Mode == "tls" {
|
|
conn = tls.Client(conn, &tls.Config{ServerName: c.Host})
|
|
}
|
|
conn.SetDeadline(time.Now().Add(30 * time.Second))
|
|
cl, err := smtp.NewClient(conn, c.Host)
|
|
if err != nil {
|
|
conn.Close()
|
|
return err
|
|
}
|
|
defer cl.Close()
|
|
if c.Mode == "" || c.Mode == "starttls" {
|
|
if ok, _ := cl.Extension("STARTTLS"); ok {
|
|
if err := cl.StartTLS(&tls.Config{ServerName: c.Host}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
if c.User != "" {
|
|
if err := cl.Auth(smtp.PlainAuth("", c.User, c.Pass, c.Host)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := cl.Mail(c.From); err != nil {
|
|
return err
|
|
}
|
|
if err := cl.Rcpt(to); err != nil {
|
|
return err
|
|
}
|
|
wc, err := cl.Data()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := wc.Write([]byte(msg)); err != nil {
|
|
return err
|
|
}
|
|
if err := wc.Close(); err != nil {
|
|
return err
|
|
}
|
|
return cl.Quit()
|
|
}
|