Files
Hammerhead/settings.go
T
VeillaxandClaude Opus 5.5 c585ddbf63
CI / test (push) Successful in 3m20s
Release / release (push) Successful in 5m6s
Review pass on the Lightwell redesign: layout, copy and behavior fixes
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>
2026-10-26 18:38:56 -06:00

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()
}